Intermediate iOS Programming with Swift 笔记 Ⅱ

最近花了点时间,把 《Intermediate iOS Programming with Swift》 刷了一遍,这是第二部分

十一、SCAN QR CODE USING AVFOUNDATION FRAMEWORK

你可以使用 AVFoundation framework 实时扫描二维码,而且整个扫描过程是基于 video capturing 的

1. import AVFoundation Framework

声明 AVCaptureSession AVCaptureVideoPreviewLayer UIView 三个实例变量

var captureSession:AVCaptureSession?  
var videoPreviewLayer:AVCaptureVideoPreviewLayer?  
var qrCodeFrameView:UIView?  

2. 实现 Video Capture

首先需要实例化一个 AVCaptureSession 对象,然后设置合适的 AVCaptureDevice 作为 input,AVCaptureSession 对象可以看做是一个协调器,在设备 video input 和 output 输出之间进行协调。

  1. 返回一个使用 AVMediaTypeVideo 的默认设备

    let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
    
  2. 使用上面的设备得到一个 AVCaptureDeviceInput 实例,该对象表示一个物理设备

    var error:NSError?
    let input: AnyObject! = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: &error)
    if (error != nil) {
    // If any error occurs, simply log the description of it and don't continue any more. 
        println("\(error?.localizedDescription)")
        return
    }
    
  3. 初始化 captureSession 对象,添加 input

    captureSession = AVCaptureSession()
    captureSession?.addInput(input as AVCaptureInput)
    
  4. 在这个例子中,输出 session 是一个 AVCaptureMetaDataOutput 对象,AVCaptureMetaDataOutput 类是读取二维码的核心,这个类连同 AVCaptureMetadataOutputObjectsDelegate 一起拦截任何来自 input 的元信息,并转换为人类可读语言

    • 初始化一个 AVCaptureMetadataOutput,并将其添加到 captureSession 的 output

      let captureMetadataOutput = AVCaptureMetadataOutput()
      captureSession?.addOutput(captureMetadataOutput)
      
    • 当有新的元信息被捕获,他会直接被转发到 delegate,所以指定 delegate 为 self,并指定 delegate 方法的执行线程 queue,而 metadataObjectTypes 告诉哪种类型的元信息是 app 感兴趣的。

      captureMetadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue()) 
      captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
      
    • 为了将video captured 展示到屏幕上,需要使用 AVCaptureVideoPreviewLayer 结合 AV capture session 显示出来

      videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
      videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
      videoPreviewLayer?.frame = view.layer.bounds
      view.layer.addSublayer(videoPreviewLayer)
      
    • 开始摄像 captureSession?.startRunning()

      messageLabel 被覆盖了,解决办法 view.bringSubviewToFront(messageLabel)

3. 实现二维码的读取

  1. 当二维码被侦测到的时候,app 会使用绿框高亮二维码范围

    qrCodeFrameView = UIView()
    qrCodeFrameView?.layer.borderColor = UIColor.greenColor().CGColor
    qrCodeFrameView?.layer.borderWidth = 2 
    view.addSubview(qrCodeFrameView!) 
    view.bringSubviewToFront(qrCodeFrameView!)
    

    此时 qrCodeFrameView 的 size 为 0 ,还不可见

  2. 二维码将会被检测出来信息,并展示在屏幕下方,通过 delegate 方法解析二维码,该方法的第二个参数 metadataObjects 包含所有的 metadata 对象 :

    func captureOutput(captureOutput: AVCaptureOutput!, 
      didOutputMetadataObjects metadataObjects: [AnyObject]!, 
      fromConnection connection: AVCaptureConnection!) {
        // Check if the metadataObjects array is not nil and it contains at least one object. 
        if metadataObjects == nil || metadataObjects.count == 0 {
            qrCodeFrameView?.frame = CGRectZero 
            messageLabel.text = "No QR code is detected" 
            return
        }
        // Get the metadata object.
        let metadataObj = metadataObjects[0] as AVMetadataMachineReadableCodeObject
        if metadataObj.type == AVMetadataObjectTypeQRCode {
            // If the found metadata is equal to the QR code metadata then update the status label's 
               text and set the bounds 
            // 将 metadata 对象的可视属性转换到 layer 上的坐标
            let barCodeObject = 
                videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj) 
                as AVMetadataMachineReadableCodeObject 
            // 最终构造出绿边
            qrCodeFrameView?.frame = barCodeObject.bounds;
            if metadataObj.stringValue != nil { 
                messageLabel.text = metadataObj.stringValue
            } 
        }
    }
    


十二、WORKING WITH URL SCHEMES

A URL scheme 是个很有趣的特性,它由 iOS 提供,并且允许开发者启动系统 app,同时第三方的 app 也可以通过该方式启动。比如你可以使用指定的 URL scheme 打开内置的 phone app 如 拨号,发送短信和邮件。同时你还能为你的 app 创建自定义的 URL scheme,这样其他应用可以通过 URL 打开你的 app。

一般系统及常用的 URL scheme 如下

Web | Tel | Facebook
---|---|---| http://www.appcoda.com | tel://743234028 | fb://feed

SMS | Mail | Whatsapp
---|---|--- sms://89234234 | mailto:support@appcoda.com | whatsapp://send?text=Hello!

使用 URL scheme 打开应用只需要调用 UIApplication 类的 openURL 方法(调用前先判断一下)

if UIApplication.sharedApplication().canOpenURL(url) {  
    UIApplication.sharedApplication().openURL(url) 
}

创建自定义的 URL Scheme

苹果允许开发者创建自己的 URLs 在不同的 app 之间进行交流,主要实现分两步

  1. 设置 App 的 Info.plist 右键添加 key,最终结果如下:

  1. 当 “Open a URL” 事件发生时,系统会调用 application(_:openURL:sourceApplication:annotation:),你需要在此方法中处理传进来的参数并做处理。例如:用 alert 显示 url

    func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject?) -> Bool { 
        let message = url.host?.stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
        let alertController = UIAlertController(title: "Incoming Message", message: message, preferredStyle: .Alert)
        let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
        alertController.addAction(okAction)
        window?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
        return true 
    }
    

    stringByReplacingPercentEscapesUsingEncoding将 url “Hello%20World!” 转换为字符串 “Hello World!”


十三、WORKING WITH CAMERA

iOS 提供了两种方式访问摄像头:

  • 最简单使用 UIImagePickerViewController
  • 可以使用 AVFoundation framework 控制内置的 cameras 和 capture images

相对于 UIImagePickerViewController 第二种更加复杂,但也更加灵活和强大。本章主要学习使用 AVFoundation framework 拍摄 静态图像。

AV Foundation media capture 的核心是 AVCaptureSession 对象,他控制着输入 (e.g. back-facing camera)和输出(e.g. image file)。拍摄静态图像的话,你需要以下步骤:

  1. 得到 AVCaptureDevice 实例
  2. 通过 device 创建 AVCaptureDeviceInput 实例
  3. 创建 AVCaptureStillImageOutput 管理输出的静态图片
  4. 使用 AVCaptureSession 来协调 -- 从输入到输出的数据流
  5. 创建 AVCaptureVideoPreviewLayer 配合 session 来显示摄像头预览

Demo

  1. 得到 AVCaptureDevice 实例:

    • 声明三个变量,前置、后置、当前摄像头

      var backFacingCamera:AVCaptureDevice? 
      var frontFacingCamera:AVCaptureDevice? 
      var currentDevice:AVCaptureDevice?
      
    • 在 AVFoundation framework 中,真实的物理设备被抽象为 AVCaptureDevice 对象,使用 devicesWithMediaType 方法穷举所有的可用摄像头:

      let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) as [AVCaptureDevice]
      
    • 遍历当前所有可用摄像头,分配给之前创建的变量

      for device in devices {
          if device.position == AVCaptureDevicePosition.Back { 
              backFacingCamera = device
          } else if device.position == AVCaptureDevicePosition.Front { 
              frontFacingCamera = device
          } 
      }
      currentDevice = backFacingCamera
      
  2. 通过 device 创建 AVCaptureDeviceInput 实例

    var error : NSError?
    let captureDeviceInput = AVCaptureDeviceInput(device: currentDevice, error: &error) 
    if error != nil {
        println("error: \(error?.localizedDescription)") 
    }
    
  3. 创建 AVCaptureStillImageOutput 管理输出的静态图片

    • 创建两个变量

      var stillImageOutput:AVCaptureStillImageOutput? 
      var stillImage:UIImage?
      
    • 在 viewDidLoad 方法中创建并配置 AVCaptureStillImageOutput 对象

      stillImageOutput = AVCaptureStillImageOutput() 
      stillImageOutput?.outputSettings = [AVVideoCodecKey: AVVideoCodecJPEG]
      
  4. 使用 AVCaptureSession 来协调 -- 从输入到输出的数据流

    • 声明 AVCaptureSession 变量 let captureSession = AVCaptureSession()
    • 在 viewDidLoad 中指定 image 的质量和分辨率 captureSession.sessionPreset = AVCaptureSessionPresetPhoto
    • 协调输入输出:

      captureSession.addInput(captureDeviceInput) 
      captureSession.addOutput(stillImageOutput)
      
  5. 创建 AVCaptureVideoPreviewLayer 并启动 session 来显示摄像头预览

    • 声明一个 AVCaptureVideoPreviewLayer 变量

      var cameraPreviewLayer:AVCaptureVideoPreviewLayer?
      
    • 设置 AVCaptureVideoPreviewLayer

      cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
      view.layer.addSublayer(cameraPreviewLayer)
      // videoGravity 指示 video preview 如何显示
      cameraPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
      cameraPreviewLayer?.frame = view.layer.frame
      
    • 启动 session 开始照相(把被覆盖的 button 移到前面)

      view.bringSubviewToFront(cameraButton)
      captureSession.startRunning()
      
  6. 按下拍照按钮开始拍摄,这里主要使用了 - captureStillImageAsynchronouslyFromConnection:completionHandler: 方法,开始静态照片的拍摄,因为是异步的,所以会立即返回。最终结果会在 completionHandler 中的 参数 CMSampleBuffer 以 buff 的形式返回(稍后会转成 NSData ),这个方法又需要用到 AVCaptureConnection 对象,而该对象表示了输入和输出之间相关的连接。

    @IBAction func capture(sender: AnyObject) {
        let videoConnection = stillImageOutput?.connectionWithMediaType(AVMediaTypeVideo)
        stillImageOutput?.captureStillImageAsynchronouslyFromConnection(videoConnection, completionHandler: 
        { (buff, error) -> Void in
            let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buff)
            self.stillImage = UIImage(data: imageData)
            self.performSegueWithIdentifier("ShowPhoto", sender: self)
        })
    }
    

    调用 jpegStillImageNSDataRepresentation方法将 buff 转成 NSData

  7. 用滑动手势触发后置摄像头到前置摄像头转换

    • 先声明,并配置添加到 view 上

      var toggleCameraGestureRecognizer = UISwipeGestureRecognizer()
      // 添加到 viewDidLoad 中 切换手势为向上滑动
      toggleCameraGestureRecognizer.direction = .Up 
      toggleCameraGestureRecognizer.addTarget(self, action: "toggleCamera") 
      view.addGestureRecognizer(toggleCameraGestureRecognizer)
      
    • 实现 toggleCamera 方法

      func toggleCamera() { 
        // 先调用此方法表明开始设置一系列
          captureSession.beginConfiguration() 
          var error: NSError?
          // Change the device based on the current camera 当前是前置则切换到后置,反之亦然
          let newDevice = (currentDevice?.position == AVCaptureDevicePosition.Back) ? frontFacingCamera : backFacingCamera
          // Remove all inputs from the session 在添加新的输入之前,先移除所有存在的input
          for input in captureSession.inputs {
              captureSession.removeInput(input as AVCaptureDeviceInput) 
          }
          // Change to the new input
          let cameraInput = AVCaptureDeviceInput(device: newDevice, error: &error)         
          if captureSession.canAddInput(cameraInput) {
              captureSession.addInput(cameraInput) 
          }
          // 确定改变
          currentDevice = newDevice
          captureSession.commitConfiguration() 
      }
      
  8. 缩放,还是添加手势,不过这次是左右滑动进行缩放。与上一步类似,同样是定义两个手势zoomInGestureRecognizerzoomOutGestureRecognizer

    // 放大
    func zoomIn() {
        if var zoomFactor = currentDevice?.videoZoomFactor {
            if zoomFactor < 5.0 {
                let newZoomFactor = min(zoomFactor + 1.0, 5.0)
                // 请求锁定设备
                currentDevice?.lockForConfiguration(nil)
                // 平滑放大
                currentDevice?.rampToVideoZoomFactor(newZoomFactor, withRate: 1.0)            
                // 释放设备
                currentDevice?.unlockForConfiguration()
            }
        } 
    }    
    //缩小
    func zoomOut() {
        if var zoomFactor = currentDevice?.videoZoomFactor {
            if zoomFactor > 1.0 {
                let newZoomFactor = max(zoomFactor - 1.0, 1.0)
                currentDevice?.lockForConfiguration(nil)
                currentDevice?.rampToVideoZoomFactor(newZoomFactor, withRate: 1.0)
                currentDevice?.unlockForConfiguration()
            } 
        }
    }
    

    在尝试设置和硬件相关的属性时,需要调用 lockForConfiguration 方法,设置完毕后记着释放锁 unlockForConfiguration

  9. 保存拍摄的相片到相册,UIKit 提供了一个方法让你添加 image 到用户照相册

    func UIImageWriteToSavedPhotosAlbum(_ image: UIImage!,
            _ completionTarget: AnyObject!,
            _ completionSelector: Selector ,
            _ contextInfo: UnsafeMutablePointer<Void>)
    

    然后我们在 save 方法中使用它

    @IBAction func save(sender: AnyObject) { 
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        dismissViewControllerAnimated(true, completion: nil)
    }
    


十四、VIDEO CAPTURING AND PLAYBACK USING AVKIT

上一章介绍了拍摄静态相片,本章我们通过更改 AVCaptureSession 的 input 和 output,来拍摄动态录像。除了拍摄影片,我们还将会学习一个 iOS 8 新的 AVKit Framework,用来回放影片。首先来拍摄录像,和拍摄静态照片步骤有些类似。

1. 设置一个 Session

  • 声明并创建 let captureSession = AVCaptureSession()
  • 设置输出分辨率 captureSession.sessionPreset = AVCaptureSessionPresetHigh

2. 选择输入设备

  • 声明 var currentDevice:AVCaptureDevice?
  • 得到所有的可用摄像头设备,然后遍历得到后置摄像头

    let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) as [AVCaptureDevice] 
    // Get the back-facing camera for taking videos
    for device in devices {
        if device.position == AVCaptureDevicePosition.Back { 
            currentDevice = device
        } 
    }
    
  • 根据 currentDevice 得到 AVCaptureDeviceInput

    let captureDeviceInput = AVCaptureDeviceInput(device: currentDevice, error: &error)
    

3. 设置输出设备

  • 声明 var videoFileOutput:AVCaptureMovieFileOutput?
  • 创建一个 AVCaptureMovieFileOutput 的实例对象,该对象用来保存数据到 QuickTime movie file。AVCaptureMovieFileOutput 类还提供了很多属性来控制录制影片的大小和长度。

    // Configure the session with the output for capturing video
    videoFileOutput = AVCaptureMovieFileOutput()
    

4. 使用 session 协调 input 和 output

// Configure the session with the input and the output devices 
captureSession.addInput(captureDeviceInput)  
captureSession.addOutput(videoFileOutput)  

5. 创建一个预览层并开始 session

  • 声明 var cameraPreviewLayer:AVCaptureVideoPreviewLayer?
  • 用 session 创建 layer,然后添加到 view 上去,并对 layer 进行设置

    cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    view.layer.addSublayer(cameraPreviewLayer) 
    cameraPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill 
    cameraPreviewLayer?.frame = view.layer.frame
    
  • 开始摄像(将 cameraButton 移到前面),其实本步骤只打开摄像头,出现当前摄像头预览,并未真正开始录像

    view.bringSubviewToFront(cameraButton) 
    captureSession.startRunning()
    

6. 保存 video 数据为一个影片文件

  • 声明一个变量标识是否正在录制视频 var isRecording = false
  • 目前 output 的 session 已经配置好了,但真正的保存过程只有调用了 AVCaptureMovieFileOutput 的 startRecordingToOutputFileURL 方法后才会执行。下面实现具体的摄像过程

    @IBAction func capture(sender: AnyObject) { 
        if !isRecording {
            isRecording = true
            // 实现了摄像时,cameraButton 的一个类似于呼吸灯似的效果(反复变大缩小)
            UIView.animateWithDuration(0.5, delay: 0.0, options: .Repeat | .Autoreverse | .AllowUserInteraction, animations: {
                self.cameraButton.transform = CGAffineTransformMakeScale(0.5, 0.5) 
            }, completion: nil)
                // 设置一个临时存放录像文件的路径
                let outputPath = NSTemporaryDirectory() + "output.mov"
                let outputFileURL = NSURL(fileURLWithPath: outputPath)
                //开始摄像
                videoFileOutput?.startRecordingToOutputFileURL(outputFileURL,recordingDelegate: self)
        } else {
            isRecording = false
            UIView.animateWithDuration(0.5, delay: 1.0, options: nil, animations: {
                self.cameraButton.transform = CGAffineTransformMakeScale(1.0, 1.0)
            }, completion: nil) 
                cameraButton.layer.removeAllAnimations()
                // 如果正在摄像,这时按下就是停止摄像
                videoFileOutput?.stopRecording()
        } 
    }
    
  • 上一步设置了 output 的 delegate ,因此一旦摄像完成后会通知代理 captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error

7. 使用 AVKit 回放

在 iOS 7 之前使用 MPMoviePlayerController 进行回放,而在 iOS 8 使用新的 AVPlayerViewController 进行回放操作。AVKit 非常简单,只有一个类 AVPlayerViewController,而该类最核心的属性就是 player(AVPlayer 类型),为了使用 AVPlayerViewController 回放录像,你只需要设置 player 属性就好了。

8. 实现 AVCaptureFileOutputRecordingDelegate

我们将在完成录像后自动播放影片,所以需要实现 captureOutput 的 delegate:AVCaptureFileOutputRecordingDelegate

func captureOutput(captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAtURL outputFileURL: NSURL!, fromConnections connections: [AnyObject]!, error: NSError!) {  
    if error != nil { 
        println("\(error.localizedDescription)") 
        return
    }

    self.performSegueWithIdentifier("playVideo", sender: outputFileURL) 
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {  
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
    if segue.identifier == "playVideo" {
        let videoPlayerViewController = segue.destinationViewController as AVPlayerViewController
        let videoFileURL = sender as NSURL
        videoPlayerViewController.player = AVPlayer(URL: videoFileURL) 
    }
}

设置 player 属性为合适 AVPlayer 对象就可以正常回放了


十五、DISPLAY BANNER ADS USING IAD

iAd 是一个由 Apple 提供的广告平台,你可以在 banner 之上加广告。

1. 使用 iAd framework

首先添加 iAd framework 框架到 Xcode中去

import iAd  

2. 显示 Banner 广告

iAd framework 是和 UIViewController 紧密相连的,UIViewController 允许使用那些专为 iAad 定制的属性。

为了显示 banner 到指定位置,你只需要设置 UIViewController 的 canDisplayBannerAds 属性为 true,controller 对象会自动把 banner 广告显示到合适的位置上

// 一旦设置好,广告会自动显示
canDisplayBannerAds = true  

3. 显示插播式广告

UIViewController 通过更改 ADInterstitialPresentationPolicy 属性还可以显示插播式广告,该属性默认为 none,你可以将其设置为 automatic。这样插播广告的任务包括插播的频率就完全交给了 iAd framework 负责。

当然,你也可以设置为 manual ,并通过 requestInterstitialAdPresentation 方法来控制合适的时间显示,比如控制在 App 启动后 30 秒后插播广告。

  • 首先设为 manual,并做好显示准备

    interstitialPresentationPolicy = .Manual
    // 显示之前获取或下载广告
    UIViewController.prepareInterstitialAds()
    
  • 为了初始化一个广告,先创建一个 help 方法

    func displayInterstitialAds() { 
        // 先禁用 banner 广告
        if displayingBannerAd {
            canDisplayBannerAds = false 
        }
        // 请求框架显示插播式广告
        requestInterstitialAdPresentation()
        canDisplayBannerAds = true 
    }
    
  • 为了在特定的时间触发广告,我们需要创建并配置一个 NSTimer 对象

    var timer = NSTimer(fireDate: NSDate(timeIntervalSinceNow: 30),
        interval: 0, target: self, selector: "displayInterstitialAds",
        userInfo: nil, repeats: false)
    NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
    

4. 在其他地方显示 banner 广告

虽然使用 canDisplayBannerAds 很容易在底部显示广告,但想在其他地方显示广告就不能依赖这个属性,需要创建自己的 banner。

  • 首先移除 canDisplayBannerAds = true
  • 遵守 ADBannerViewDelegate 并添加两个变量

    ADBannerView 提供了用来显示 banner 广告的 view,iAdDisplayed 用来表示广告是否显示,任何显示广告的事件都有 ADBannerViewDelegate 沟通

    @interface iAdDemoTableViewController : UITableViewController <ADBannerViewDelegate>
    @property (nonatomic, strong) ADBannerView *adBannerView; 
    @property (nonatomic, assign) BOOL isAdDisplayed;
    @end
    
  • 实现,将 banner 广告插入表头

    adBannerView = ADBannerView(adType: ADAdType.Banner)
    adBannerView?.delegate = self
    
    
    override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return adBannerView 
    }
    

5. 显示、隐藏 Banner 广告

为了增强用户体验,我们可以设置为只有真正刷出广告才显示,对上面的方法更改

// 用该方法控制 banner 是否显示
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {  
    // 只有 isAdDisplayed 存在才会显示表头
    if isAdDisplayed {
        if let bannerView = adBannerView { 
            return bannerView.frame.size.height
        }
    }
    return 0 
}

设置 Banner 的代理方法

// 当新广告显示时,调用
func bannerViewDidLoadAd(banner: ADBannerView!) {  
    println("Banner ad loaded successfully") 
    isAdDisplayed = true

    // Reload table section to show the banner ad 刷新显示
    let indexSet = NSIndexSet(index: 0) 
    tableView.reloadSections(indexSet, withRowAnimation: .Automatic)
}
// 当广告显示失败时,调用
func bannerView(banner: ADBannerView!, didFailToReceiveAdWithError error: NSError!) {  
    println("Failed to load banner ad")
    isAdDisplayed = false

    // Reload table section to hide the banner ad
    let indexSet = NSIndexSet(index: 0) 
    tableView.reloadSections(indexSet, withRowAnimation: .Automatic)
}


十六、USING CUSTOM FONTSTargets

  • 添加自定义字体,通常将自定义字体放到一个叫 font 的文件夹下面,然后拖到 Xcode 中
  • 注册这些字体到工程中去

    在 Targets 下选择 Info,新建一个 “Fonts provided by application” 属性,这是一个 Key 数组,允许你注册自定义字体。右键 Add Row ,添加所有的字体名称到 value 中去,最终结果如下:

  • 在 IB 中使用自定义字体, Xcode 6 支持实时将自定义字体显示在 IB 上,你甚至可以在 Attributes inspector 上修改自定义字体
  • 在 Code 中使用自定义字体,同样你可以通过以下代码使用自定义的字体

    label1.font = UIFont(name: "Mohave-Italic", size: 25.0) 
    label2.font = UIFont(name: "Hallo sans", size: 30.0) 
    label3.font = UIFont(name: "CanterLight", size: 35.0)
    

    在字体文件上右键可以看到对应字体的 Full Name


十七、AIRDROP

iOS 使用 AirDrop 这种技术在不同 iOS 设备间进行数据分享,UIActivityViewController 类可以很容易地将 AirDrop 嵌入到你的 APP 中,这个类掩盖了细节,你所需要做的仅仅是告诉他你需要分享什么,剩下的工作交给 UIActivityViewController 就 OK 了。为了激活 AirDrop,你需要去控制中心选择分享数据的对象是 “Contact Only” or “Everyone”。

AirDrop 使用蓝牙技术扫描周边设备,发现有可用连接,就创建一个 ad-hoc 的 WiFi 网络去连接另一台设备,允许更快的数据传输。使用 AirDrop 并不需要 WiFi,只有在数据传输时才需要 WiFi。另外 AirDrop 不支持锁屏,所以分享数据时,需保持两台设备都在屏幕开启状态。

1. UIActivityViewController 概述

UIActivityViewController 是由 UIKit framework 提供的一个类,用来整合这一过程,这个类提供了一些基本服务,包括拷贝到剪贴板,分享到社交网络,通过 Messages 发送消息,iOS 7 开始支持 AirDrop,从 iOS 8 开始添加了一些 app extensions 支持。这个类使用起来也很简单:

  • 创建一个 UIActivityViewController 实例,并显示到屏幕上,

    let objectsToShare = [fileURL]
    // 用数组初始化并显示到屏幕上
    let activityController = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
    presentViewController(activityController, animated: true, completion: nil)
    

    如果一个临近的设备被检测到了,这个 activity controller automatically 会自动显示,允许分享的话,则自动处理数据传输。

  • 默认的 activity controller 包含一些选项,如 Messages, Flickr and Sina Weibo 等,如果不需要可以通过设置属性 excludedActivityTypes 排除一部分

    let excludedActivities = [UIActivityTypePostToWeibo, UIActivityTypeMessage, UIActivityTypePostToTencentWeibo]
    activityController.excludedActivityTypes = excludedActivities
    
  • 你可以使用 UIActivityViewController 分享不同类型的数据,包括 String, UIImage and NSURL。而且对于 NSURL,你不仅仅是能够分享链接,你可以通过文件的 URL 地址分享各种类型的文件。

  • 在接受数据的一端,当设备接受到数据会自动打开数据类型所对应的软件,比如接收到 UIImage 会打开相册,接收到 PDF 会打开 Safari 等等。

2. Demo

添加一个分享按钮,用来实现 AIRDROP 的特性

  • 首先来实现一个 help 方法 fileToURL: 即给定一个文件名,返回 url 路径

    func fileToURL(file: String) -> NSURL? {
        // Get the full path of the file
        let fileComponents = file.componentsSeparatedByString(".")
        if let filePath = NSBundle.mainBundle().pathForResource(fileComponents[0], ofType:fileComponents[1]) { 
            return NSURL(fileURLWithPath: filePath)
        }
        return nil 
    }
    
  • 实现分享

    @IBAction func share(sender: AnyObject) { 
        //根据文件名找到文件 url
        if let fileURL = fileToURL(self.filename) {
            let objectsToShare = [fileURL]
            //创建 UIActivityViewController 实例
            let activityController = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
            // 排除不需要的选项
            let excludedActivities = [UIActivityTypePostToFlickr, UIActivityTypePostToWeibo,
                                      UIActivityTypeMessage, UIActivityTypeMail,
                                      UIActivityTypePrint,UIActivityTypeCopyToPasteboard,
                                      UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll,
                                      UIActivityTypeAddToReadingList,UIActivityTypePostToFlickr, 
                                      UIActivityTypePostToVimeo, UIActivityTypePostToTencentWeibo]
            activityController.excludedActivityTypes = excludedActivities
            //展示到屏幕上
            presentViewController(activityController, animated: true, completion: nil) 
        }
    }
    

3. 统一类型标识符

iOS 是如何知道哪种数据类型是用哪种对应的数据类型打开的,UTIs (short for Uniform Type Identifiers) 是苹果对特定数据的回应系统,通常一个统一的类型标识对与数据类型来说是唯一的,例如 com.adobe.pdf 表示 PDF 文件,public.png 表示 PNG,完整的列表见这里,系统也允许多个应用注册相同的 UTI,这样的话,iOS 会弹出一个列表,供用户选择用哪种 App 打开。


十八、BUILDING GRID LAYOUT USING COLLECTION VIEW

1. 熟悉 UICollectionView 和 UICollectionViewController

UICollectionView 类似于 UITableView,只不过 UITableView 管理着一堆数据然后以单行的方式显示到屏幕上,而 UICollectionView 提供了可自定义 layouts 的方式显示,你能够以 multi-column gridstiled layoutcircular layout 方式显示数据。

UICollectionViewFlowLayout 默认管理着每一个 section 中的 item 排列,也包括 header 和 footer views

UICollectionView 主要由以下几部分组成:

  • Cells 基本元素
  • Supplementary views 通常用来实现 section 的 header 或 footer
  • Decoration views 仅仅用来装饰,和数据集合不相关

2. CollectionViewController 的基本实现

  • 可以直接从 Object Library 拖一个 Collection View Controller 到 Storyboard 上
  • 与 UITableView 类似,也要实现 UICollectionViewDataSource 方法

3. 自定义 UICollectionViewCell 的背景

通常一个 collection view cell 由三个不同的部分组成,包括 background, selected backgroundcontent view。通常我们可以设置一个背景图片,并将cell content view 包含的 view 设的小一些,就能看到背景色了。


十九、INTERACTING WITH COLLECTION VIEW

1. 处理选中 cell 的操作

这里将要做两个操作

  • 点击一个 cell 上的照片, 弹出一个更大尺寸的照片
  • 选中多个照片,分享到 Facebook

第一步实现主要通过 segue,现在看一下选择多个照片

2. 处理多个选中

UICollectionView 支持选中单个和多个 itme,默认只允许用户选中一个 item,可以设置 allowsMultipleSelection 属性为 true 开启多个选中。

  • 现在 demo app 主要提供两种模式 single selection 和 multiple selections。设置两个变量:
    • shareEnabled 用来指示多选模式是否开启
    • selectedRecipes 数组,用来存储被选中的 item
  • UICollectionViewDelegate 允许你处理选中和高亮 items 等操作,cell 有个 selectedBackgroundView 属性可以设置被选中后的背景

主要实现思路:分享模式下 (shareEnabled 为 true),选中的 item 都添加到 selectedRecipes 数组中,取消选中则从数组中移除,分享时,遍历 selectedRecipes 数组,添加到 SLComposeViewController 对象相应的属性上。


二十、ADAPTIVE COLLECTION VIEW

构建一个自适应的 Collection View 主遵循:

  • cell 是自适应的 随具体设备和相应方向改变 size(主要使用 size classes 和 UITraitCollection)
  • app 是通用的(同时支持 iPhone 和 iPad)
  • 使用 UICollectionView 构建,需要手动实现 datasource 和 delegate

为 Size Classes 设计

为了防止 cell 的尺寸在 大屏幕设备上被 autolayout 拉伸,激活 cell size adaptive,我们将在竖屏和横屏模式下各自保持 cell 的 size

在 iOS 8 之前,采取下面的方式判断设备类型和方向:

let device = UIDevice.currentDevice()  
let orientation = device.orientation  
let isPhone = (device.userInterfaceIdiom == UIUserInterfaceIdiom.Phone) ? true : false

if isPhone {  
    if orientation.isPortrait {
        // Change cell size 
    }
}

而在 iOS 8 之后采取 size classes 来处理这一切 那我们如何从 code 得到当前 size class

理解 trait Collection

我们使用一个新的系统叫做 Traits,水平和垂直的 size classes 都可以看做是 traits,像很多属性如 userInterfaceIdiom、display scale 等他们一起组成了一个 trait collection 集合。

在 iOS 8 苹果介绍了 Trait Environments 也就是 UITraitEnvironment,这是一个新协议,能够返回当前的 Trait Collection,因为 UIViewController 遵循这个 UITraitEnvironment 协议,你可以使用 traitCollection 属性访问当前 trait collection。你可以直接在 ViewDidLoad 方法中试验

println("\(traitCollection)")

// result:
<UITraitCollection: 0x7f857b5b6860; _UITraitNameUserInterfaceIdiom = Phone, _UITraitNameDisplayScale = 3.000000,  
_UITraitNameHorizontalSizeClass = Compact, _UITraitNameVerticalSizeClass = Regular, _UITraitNameTouchLevel = 0,  
_UITraitNameInteractionModel = 1>  

自适应的 Collection View

只要理解了 trait collection,你就该知道如何决定当前设备的 size class,现在来把上面的 collection view 变成自适应的吧,UICollectionViewDelegateFlowLayout 协议提供了一个可选方法,可以指定每一个 cell size,你要做的就是覆盖并在运行时返回一个 cell size,回想一下我们只在 iPhone 竖直模式下更改 cell 的尺寸

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {  
    let sideSize = (traitCollection.horizontalSizeClass == .Compact && traitCollection.verticalSizeClass == .Regular) ? 80.0 : 128.0
    return CGSize(width: sideSize, height: sideSize) 
}

水平方向是 compact,垂直方向是 regular,证明设备是竖直模式,cell size 为 80,相反其他模式下 cell size 都是 128

响应 Size Class 的改变,我们可以覆盖 UIContentContainer protocol 的如下方法回应 size class 的改变:

override func viewWillTransitionToSize(size: CGSize,  
    withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        collectionView.reloadData() 
}

不过书里代码这里有个问题,就是横屏的时候,滚动到最下面,然后在把设备转为竖屏,因为竖屏模式下,屏幕内容是完全能显示的,因此系统会判断滚动不能用。尴尬的是,此时竖屏过来的位置刚好是最后几行在最上面,滚动就停止了。自己修复了一下:

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {  
    collectionView.reloadData()
    println("\(collectionView.contentOffset.y)")
    coordinator.animateAlongsideTransition(nil, completion: { (context) -> Void in
        if !(self.collectionView.contentSize.height > self.collectionView.bounds.height) {
            UIView.animateWithDuration(0.5, animations: { () -> Void in
                self.collectionView.contentOffset.y = -64
            })
        }
    })
}

-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!