2015年2月19日 星期四

UIView、UIWindow、UIScreen初探

UIView定義螢幕上一個矩型區域,並且管理該區域的內容;在執行時期UIView物件負責呈現該區域所有內容,並且處理與該區域的所有互動;一個UIView也可包含多個UIView物件,形成階段式的UIView(UIView Hierarchy)。


簡單來說螢幕上看到的都是UIView。


UIWindow管理及協調在App中的所有UIView物件,並與UIScreen合作顯示在實際的螢幕上,除非外接螢幕App只會有一個UIWindow物件,UIWidnow兩個主要的功能,一是容器顯示其下所有UIView物件,二是將事件分派到其下的UIView物件;UIWidnow也繼承至UIView,擁有UIView的所有特性。


簡單來說一個App只會有一個UIWindow,負責管理所有的UIVew,及分派事件給它們。


UIScreen定義實際螢幕屬性,iOS裝置有主要螢幕,及零或多個附加螢幕;UIScreen可用來取得主要的螢幕物件,及其他附加螢幕物件。每個UIScreen會定義螢幕的長寬及其他螢幕屬性,像是亮度。


UIWindow有screen屬性(UIScreen物件),代表顯示內容的螢幕;而UIView有window屬性,代表所屬的UIWindow。參考下圖會更清楚UIWindow、UIView在App中的位置。


core_objects_2x.png


以下介紹如何取得UIWindow及UIScreen,及它們常用的方法。

取得UIWindow



var window = UIApplication.sharedApplication().keyWindow


var window = UIApplication.sharedApplication().delegate?.window?


若在UIViewController中可以用以下方法取得UIWindow。


var window = self.view.window


取得UIWindow後可用下面的方法測試。


println("\(window!.subviews.count)")


UIWindow與UIView關係



若用Single View Application的subviews.count輸出其值是1,我們將此UIView描述輸出。


println("UIWindow First Subview Description:\(window!.subviews[0].description!)")


UIWindow First Subview Description:<UIView: 0x7fb83ae34540; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb83ae34810>>
<UIScreen: 0x7fb83ad1bf20; bounds = {{0, 0}, {375, 667}}; mode = <UIScreenMode: 0x7fb83ac12dd0; size = 750.000000 x 1334.000000>>


下圖示意Single View Application,最底層是UIWindow,接下來放置UIView,最後才放置其它視覺化物件。


UIWindow及UIView.png


新建UIWindow



App建立時會自動為我們建立UIWindow,我們也可以建立自己的UIWindow來取代原本的UIWindow,參考以下程式碼。


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {


self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window!.backgroundColor = UIColor.redColor()
self.window!.makeKeyAndVisible()
return true
}

UIWindow的rootViewController屬性



UIWindow要替換其中所有的內容,建議方式是新建一個ViewController,然後指定到rootViewController屬性,而不是直接新增Subviews,若直接新增則需自行維持View與ViewController之間的關連。以下程式碼將原本ViewController置換。


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {


var navController: UINavigationController = UINavigationController()
self.window!.rootViewController = navController
let myLabel: UILabel = UILabel(frame: CGRectMake(0,0, 100,100))
myLabel.text = "Hello World"
navController.view.addSubview(myLabel)
navController.setToolbarHidden(false, animated: true)
return true
}


下圖說明rootViewController與subviews之間的差異。


rootViewController.png


取得UIScreen


var screen = UIScreen.mainScreen()


取得螢幕大小



bounds取得包含statusbar的App大小,applicationFrame取得不包含statusbar的App大小;可用以下程式碼做實驗。


var r = UIScreen.mainScreen().applicationFrame


println(r)
       
var r1 = UIScreen.mainScreen().bounds
       
println(r1)


輸出結果如下ApplicationFrame輸出寬度是375、高度是647,Y軸位置是20;而Bounds輸出寬度是375、高度是667,Y軸位置是0。


(0.0,20.0,375.0,647.0)  -> ApplicationFrame
(0.0,0.0,375.0,667.0)    -> Bounds


需要注意的是在iOS8後輸出的寬度及高度並非不變的,而是根據螢幕旋轉而變動,我們將螢幕旋轉90度後,輸出的值如下。


(0.0,0.0,667.0,375.0) ->  ApplicationFrame
(0.0,0.0,667.0,375.0) ->  Bounds


所以我們可以這特性很簡單的判斷螢幕是直向還是橫向。


      if (r.width < r.height)
      {


           println("Portrait 直向")


      }
      else
      {
           println("Landscape 橫向")
      }


截取螢幕畫面


UIScreen提供螢幕截取方法snapshotViewAfterScreenUpdates,其中afterUpdates是否要將最近的變動更新後再截取,常用的值是false要立即截取目前畫面;傳回的是UIView,需要自行轉存成影像檔案,並存到相簿中,參考以下程式碼。


       //截取螢幕畫面
       var currentview = UIScreen.mainScreen().snapshotViewAfterScreenUpdates(false)


      //建立UIImage
       UIGraphicsBeginImageContext(currentview.frame.size)
       currentview.layer.renderInContext(UIGraphicsGetCurrentContext())
       let image = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()


       //儲存到相簿
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)

2015年2月16日 星期一

UIApplication初探

本文分享如何使用UIApplication,每個App都會有一個UIApplication實體,不需要建立,而是透過sharedApplication方法取得。

取得UIApplication實體


var currentApp = UIApplication.sharedApplication();

Local and Push Notification


當App在背景執行時,也能主動通知使用者,此為Local and Push Notification;因需較為深入探討,再撰寫專文介紹,可先參考如下連結。

開啟超連結


要在App中開啟超連結,可以用openURL方法,參考以下程式碼。

let url = "http://google.com"

let param:NSURL = NSURL(string:url)!

UIApplication.sharedApplication().openURL(param)

除了開啟超連結外,也可以用此方法來打電話、傳簡訊、FaceTime…等;詳細可參考Apple URL Scheme Reference,整理範例如下。

Links類型
範例
Email
mailto:web@kunhsiang.com
Phone
tel:1-408-555-5555
FaceTime
facetime://user@example.com
SMS
sms:1-408-555-1212
Map
http://maps.apple.com/?daddr=San+Francisco,+CA&saddr=cupertino
iTunes
http://phobos.apple.com/WebObjects/MZStore.woa/wa/viewAlbum?i=156093464&id=156093462&s=143441
Youtube
http://www.youtube.com/watch?v=VIDEO_IDENTIFIER

openURL不僅可以開啟常用的應用程式,它更是應用程式間溝通的方式,我們可以自訂URL用來開啟別的App並且傳遞參數,以下面URL其中todolist是代表的應用程式名稱,後面是參數,詳細可參考Using URL Schemes to Communicate with Apps

todolist://www.acme.com?Quarterly%20Report#200806231300

移除Statusbar


可以將App上方的Statusbar移除,方法有兩種,一種是從Application的角度來實作,另一種則是則從ViewController角度來實作。

從Application角度來實作,先在Application加入Key為View controller-based status bar appearance,Value為No;然後在AppDelegate加入以下程式碼。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
       application.statusBarHidden = true
       return true
}

從ViewController角度來實作較為簡單,直接在ViewController加入程式碼。

override func prefersStatusBarHidden() -> Bool {
   return false
}

或是

override func  preferredStatusBarStyle() -> UIStatusBarStyle {
       return UIStatusBarStyle.LightContent
}

應用程式狀態


UIApplication物件可以用applicationState方法取得目前App的狀態,分為Active、InActive及Background;簡單來說Active是App在前景執行,並且能接收事件、InActive也是在前景執行,但是無法接收事件,可能被來電中斷或是正在執行某段程式碼正在等待中、Background則是按下Home鍵後App會在背景執行。

var AppStatus = ""
       
if (currentApp.applicationState == UIApplicationState.Active)
{
 AppStatus = "Application Status:Active"
}
else if (currentApp.applicationState == UIApplicationState.Inactive)
{
 AppStatus = "Application Status:Inactive"
}
else if (currentApp.applicationState == UIApplicationState.Background)
{
 AppStatus = "Application Status:Background"
}

搖晃偵測


UIApplication可用來設定是否能偵測搖晃,屬性如下。

app.applicationSupportsShakeToEdit = YES;

但是實際上可從ViewController來實作為佳,參考以下程式碼。

   override func viewDidAppear(animated: Bool) {
       super.viewDidAppear(animated)
       self.becomeFirstResponder()
   }
   
   override func canBecomeFirstResponder() -> Bool {
       return true
   }
   
   override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent) {
       if(event.subtype == UIEventSubtype.MotionShake) {
           var alert = UIAlertController(title: "Shaken",
               message: "Not Stirred",
               preferredStyle: UIAlertControllerStyle.Alert)
           alert.addAction(UIAlertAction(title: "OK",
               style: UIAlertActionStyle.Default, handler: nil))
           self.presentViewController(alert, animated: true, completion: nil)
       }
   }


關閉自動睡眠


iPhone/iPad在一段時間未使用時,因為省電的原因會自動進入睡眠狀態,若要關閉自動睡眠功能可用以下屬性。

application.idleTimerDisabled = true

網路狀態指示


可顯示或移除網路狀態指示,運用以下屬性。

application.networkActivityIndicatorVisible = false

iOS App應用程式生命週期(下)

The Main Run Loop

App的main run loop用來處理所有使用者相關的事件,UIApplication物件在開始時會設定main run loop,用來處理事 bv件及畫面的更新;就像名稱所提及,main run loop在App的main thread執行。這樣的機制確保使用者相關的事件,能夠循序的在程式中處理。下圖說明了man run loop的流程。
使用者與iPhone/iPad互動時,系統會根據使用者的互動產生相關事件,並由UIKit傳遞給App。
事件在內部會放到Event queue,並依序傳給main run loop去執行; UIApplication 是第一個接受到這些事件的物件,並且決定誰會處理這些事件。以Touch event為例通常會派送到main window物件,然後會送到發生事件的view上,其他事件也是用類似方式。

Processing events in the main run loop







常見事件可以看到下表,大部份的事件用main run loop傳遞到app,但是一些則不是;一些事件是透過App開發者提供的委託物件或是程式碼片段。詳細可參考Event Handling Guide for iOS
事件類別
傳遞給...
註解
Touch
事件發生時的view object
當觸碰螢幕時會發生事件,最初接觸到的Views 是responder objects,當第一個接觸到的未處理,會轉給上層的responder objects來處理。
Remote control
Shake motion events
Remote control events是由耳機按鈕或其他配件所產生的事件;Shake motion events則是使用者搖晃事件。
Accelerometer
Magnetometer
Gyroscope
開發者指定的物件
Accelerometer加速感應器可用來精確測量出步行和跑步的距離、Magentometer則是磁力儀,用來偵測附近的磁場、Gyroscope陀螺儀用來指向固定方向,可用來實作指南針。
Location

開發者指定的物件
使用Core Location framework來接受location events,更多的資訊可參考Location and Maps Programming Guide
Redraw


當View需要更新
重繪事件(Redraw events)不會引發事件相關物件,只是簡單呼叫view去重畫它自己。詳細可以參考Drawing and Printing Guide for iOS

一些事件,像是touch及remote control事件,會由App的responder objects來處理;Responder objects在App中隨處可見,像是UIApplication object、view objects及view controller objects。
大部份事件會傳遞給特定的Responder object,但是也可傳遞給其他的Responder objects(稱之為responder chain)。
控制項(像是Buttons)的Touch事件的處理方式和其他類型的Views不一樣,通常只有固定的幾項互動在這控制項,這些互動會被包裝成Action Messages並傳遞到正確的Target Object。Target-action design pattern讓控制項很容易去觸發在App的自訂程式碼。

Apps 執行狀態

狀態
描述
Not running
App尚未執行。
Inactive
App正在前景執行但是沒有接受事件(可能正在執行其他程式碼);App通常只會在此狀態一小段時間,就會轉為其他狀態。
Active
App正在前景執行,也正在接受事件。
Background
App正在背景執行程式碼,大部份App短暫進入此狀態後就會進入暫停狀態;對於如何在背景狀態執行程式碼,可以參考Background Execution
Suspended
App正在背景但是沒有執行程式碼,系統會自動將Apps進入此狀態,並且不會通知它們。當進入暫時狀態時,App仍然在記憶體中,但是不會執行任何程式碼。當記憶體不足時,系統可能會不通知而直接中斷暫停的App,為了提供更多的記憶體空間給前景App。


大部份App狀態轉換是透過呼叫app delegate object的方法來達成;這些方法是程式撰寫者的機會去處理這些轉換,這些方法簡單說明如下。

App Termination

App必須要準備好隨時被使用者或系統所結束,而且不能等待儲存使用者資料或做其他重要的工作;由系統所引發的結束是App應用程式生命週期正常的一部份。
系統通常會終止App以釋放更多的記憶體空間給其他的應用程式使用,暫停的Apps被中止時不會接受到任何的通知。
假設App正在背景執行,並且尚未暫停,若要中止系統會呼applicationWillTerminate方法。當系統重啟時也不會呼叫此方法。
除了系統中止App外,使用者也可以直接使用介面來中止App;由使用者所中止的App就和系統中止暫停的App是一樣的,App並不會接受到任何的通知。 

Threads and Concurrency

系統建立App的main thread,而開發者建立額外的thread用來去執行其他工作;就iOS Apps,較傾向的方式是用Grand Central Dispatch (GCD),GCD的觀念是讓你去定義你想做的工作,以及這些工作想執行的優先次序,但是由系統來實際決定最佳的執行方式,而不是自行建立及管理thread。讓系統來管理thread可以簡化開發者要撰寫的程式碼,並且也會有較佳的程式碼正確性及系統效能。當考慮到threads及concurrency,想像以下的情況。
  • 操作Views、Core Animation及許多其他UIKit類別時通常會在app的主thread進行操作,但是有些例外,例如以影像為基礎的操作常會在背景thread。
  • 長時間的工作應該總是在背景thread執行,例如網路存取、檔案存取、大量資料存取,應用GCD來做同步處理。
  • 在App啟動時,盡可能不要在main thread執行任務;在App啟動時應該快將使用者介面設定好。在main thread應該只執行設定使用者介面相關動作,其他任務應在其他的thread採用同步執行。

關於更多GCD同步的技術可以參考Concurrency Programming Guide