iOS App Development 1
iOS App Development
1
2
資料下載
https://dl.dropboxusercontent.com/u/14692540/iphoneui.zip
Who am I?林銘賢
白天網路公司工程師
晚上南台講師
其它時間導航 App Navier HUD iOS 版 作者
Navier HUD iOS
Navier HUD iOS
Navier HUD iOS
Navier HUD iOS
8
課程大綱 iOS App 基礎
Tab Bar
Page Control
Table
Gesture
Collection
Timer and Thread
iAd and IAP
9
iOS App Basics
10
View 階層架構
11
View 階層架構
12
畫面坐標原點在左上角
13
Frame 與 Bounds
14
App 沙盒 (SandBox)
每個 App 都有自獨立的目錄
15
常用目錄
目錄名稱 說明<Application_Home>/<AppName>.app App bundle 。不能在這個目錄
寫入任何資料<Application_Home>/Documents 存放一般資料,不會被系統清除<Application_Home>/Library 存放與 code 有關,但非使用
者資料的地方<Application_Home>/tmp 暫存的目錄,會被系統清除
16
轉向直立
直立上下顛倒
橫向 - 左 橫向 - 右
17
啟動 APP
18
切換 App 執行進入背景模式
19
從背景模式切換到前景模式
20
21
視窗架構
22
視窗架構
實際上是由 View Controller 負責提供 UIView 給 Windows
23
init
loadView
viewDidLoad
viewWillAppear
viewDidAppear
viewWillDisappear
viewDidDisappear
viewDidUnload
dealloc
View Controller 的生命週期
24
View Controller 架構
25
View Controller 架構
26
27
28
29
Navigation
Push
Pop
Push
Pop
30
31
32
Modal
33
Segue
Segue
Push 把目標 View Controller 加到 Navigation 堆疊最上層
Modal 顯示目標 View Controller
Custom 由使用者自定訂義 View Controller 之間的轉換
34
Segue Push 與 Modal 的比較
Push較有結構,有 Navigation 堆疊幫忙回上一層 View
Controller有 Navigation Bar需要有 Navigation View Controller
Modal可任意帶出下一個 View Controller需自行定義 View Controller 之間的關係
35
專案設定
36
專案設定Xcode 5.1
iPhone Retina 4 inch, 64 bit
37
38
設定 framework加入
iAd.framework ( iAd 廣告 )
StoreKit.framework (In App Purchase, IAP, 內購 )
39
專案資料夾
40
變更預設 StoryBorad
41
設定 App Icon
42
設定 App 圖示與起始畫面
圖示
起始畫面
43
設定 App 圖示與起始畫面
44
設定起始畫面
45
Tab Bar
46
Tab Bar 範例
47
Tab Bar 架構
48
Tab Bar 設計
49
50
51
新增 Tab Bar 的 View Controller
滑屬右鍵
52
新增 Tab Bar 的 View Controller
53
54
Page Control
55
Page Control
56
使用到的元件
57
58
59
60
新增界面元件與 View Controller 的關係
選擇 Automatic, 讓 Xcode 自動幫你列出相關的 View Controller
61
新增界面元件與 View Controller 的關係
建立 ScrollView Outlet
設定 ScrollView 的 delegate
62
PageViewController.h
@interface PageViewController : UIViewController<UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIPageControl *pageControl;
@end
PageViewController.m
@interface PageViewController ()
{
NSArray* imageArray;
}
@end
63
PageViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// 建立圖片陣列 imageArray = [[NSArray alloc] initWithObjects:
@"wallpaper1.jpg",
@"wallpaper2.jpg",
@"wallpaper3.jpg",
@"wallpaper4.jpg",
@"wallpaper5.jpg",
@"wallpaper6.jpg",
@"wallpaper7.jpg",
@"wallpaper8.jpg",
nil];
64
// 依序把圖片放到 Scroll View 的相對應位置 for (int i = 0; i < [imageArray count]; i++)
{
// 計算圖片大小及原點位置 CGRect frame;
frame.origin.x = self.scrollView.frame.size.width * i;
frame.origin.y = 0;
frame.size = self.scrollView.frame.size;
// 建立 UIImageView 物件存放圖片 UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame];
imageView.image = [UIImage imageNamed:[imageArray objectAtIndex:i]];
[self.scrollView addSubview:imageView];
}
// 設定 Scroll View 的內容物大小 self.scrollView.contentSize=CGSizeMake(320*8, 460);
// 設定 Scroll View 委派物件為自己 self.scrollView.delegate = self;
}
65
PageViewController.m
// 處理捲動事件- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// 取得目前顯示頁面的坐標 CGFloat pageWidth = self.scrollView.frame.size.width;
// 計算出頁碼 int page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
// 設定頁碼 self.pageControl.currentPage = page;
}
UIScrollViewDelegate
66
Table
67
功能Table 資料的顯示
顯示詳細內容
移動
刪除
Navigation Bar
跨 View Controller 之間的資料傳遞
68
69
70
71
嵌入 Navigation Controller
72
73
Table ViewUITableViewDataSource
提供要顯示的資料
UITableViewDelegate處理 TableView 上的操作及事件
74
Table View設定 dataSource 與 delegate
75
76
新增 Push Segue
77
新增 Push Segue
78
79
設定自訂的 View Controller 類別
MyTableViewController繼承 UITableViewController
80
MyTableViewController.m
@interface MyTableViewController ()
{
NSMutableArray *titles;
NSString *selectedString;
}
@end
81
準備 Table 資料
82
MyTableViewController.m
- (void)viewDidLoad
{
NSMutableArray *section;
[super viewDidLoad];
titles = [[NSMutableArray alloc] initWithCapacity:3];
// 第一個 Section 台南 section = [NSMutableArray arrayWithObjects:
@" 東區 ", @" 中西區 ", @" 北區 ", @" 南區 ", @" 永康區 ", @" 安平區 ", nil];
[titles addObject:section];
// 第二個 Section 高雄 section = [NSMutableArray arrayWithObjects:@" 三民 ", @" 前鎮 ", @" 苓雅 ", nil];
[titles addObject:section];
// 第三個 Section 屏東 section = [NSMutableArray arrayWithObjects:@" 三地門 ", @" 恆春 ", nil];
[titles addObject:section];
// 設定 Navigation Bar 的右邊按鈕為 Table View 的編輯按鈕 self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
83
MyTableViewController.m// 設定 Section 數- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return titles.count;
}
// 設定 Section 內的列數- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return ((NSMutableArray*)[titles objectAtIndex:section]).count;
}
// 設定要顯示 cell- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 透過 Identifier: DefaultCell 取得 Cell UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"DefaultCell" forIndexPath:indexPath];
// 設定區域名稱 cell.textLabel.text =
[[titles objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
return cell;
}
UITableViewDataSource
UITableViewDataSource
UITableViewDataSource
84
MyTableViewController.m
// 設定各 Section 的名稱- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString *sectionName;
switch (section)
{
case 0:
sectionName = @" 台南 ";
break;
case 1:
sectionName = @" 高雄 ";
break;
default:
sectionName = @" 屏東 ";
break;
}
return sectionName;
}
UITableViewDataSource
85
處理資料編輯 ( 刪除 )
86
MyTableViewController.m// 設定是否允許編輯列 ( 刪除 )
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
// 確定輯輯後狀態- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
// 編輯的類型為『刪除』 if (editingStyle == UITableViewCellEditingStyleDelete) {
// 更新 Section 資料 NSMutableArray *section;
section = [titles objectAtIndex:indexPath.section];
[section removeObjectAtIndex:indexPath.row];
if (section.count == 0)
{ [titles removeObject:section]; }
// 更新表格狀態 [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
UITableViewDelegate
UITableViewDelegate
87
處理資料移動
88
MyTableViewController.m
// 設定是否允許移動列- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
// 移動列範圍判斷- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath
{
// 不在同一個 Secion 的話,不允許移動 if( sourceIndexPath.section != proposedDestinationIndexPath.section )
return sourceIndexPath;
else
return proposedDestinationIndexPath;
}
UITableViewDelegate
UITableViewDelegate
89
MyTableViewController.m
// 移動列- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
NSMutableArray* section;
// 判斷是否在同 Secion 內移動 if (fromIndexPath.section == toIndexPath.section)
{
NSString *fromString;
section = [titles objectAtIndex:fromIndexPath.section];
fromString = [section objectAtIndex:fromIndexPath.row];
// 把被移動的列從原本的 Secion 中刪除 [section removeObjectAtIndex:fromIndexPath.row];
// 把被移動的列加到新位置 [section insertObject:fromString atIndex:toIndexPath.row];
}
}
UITableViewDelegate
90
處理資料選擇及跨 View Controller 之間的
資料傳遞
91
MyTableViewController.m
// 處理按下列時的事件- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 儲存該列的地名 selectedString = [[titles objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
// 執行 segue, 移動到 DetailViewController [self performSegueWithIdentifier:@"segueDetail" sender:self];
}
UITableViewDelegate
92
MyTableViewController.m
// 準備處理 Segue- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// 判斷即將要執行的 Segue 是否為 segueDetail if ([[segue identifier] isEqualToString:@"segueDetail"])
{
// 取得目標 ViewController DetailViewController* detailViewController = [segue destinationViewController];
// 設定要顯示的地名 detailViewController.text = selectedString;
}
}
Segue
93
DetailViewController.h
@interface DetailViewController : UIViewController
@property (weak, nonatomic) IBOutlet UILabel *textLabel;
@property (weak, nonatomic) NSString *text;
@end
DetailViewController.m
- (void)viewWillAppear:(BOOL)animated
{
// 設定標籤顯示地名 self.textLabel.text = self.text;
}
94
Gesture手勢處理
95
手勢類型Tap 點擊 Rotation 旋轉 Pinch 縮放
96
手勢類型Swipe (掃動 )
固定方向上下左右
Pan (拖拉 )任何方向上下左右斜對角
97
處理手勢的類別
98
元件
99
Tap 點擊不連續手勢類型
100
狀態轉換
101
Pinch 縮放連續手勢類型
102
103
UIGestureRecognizerState
UIGestureRecognizerStateBegan 開始UIGestureRecognizerStateChanged 改變中,連續型的才有UIGestureRecognizerStateEnded 結束UIGestureRecognizerStateCancelled 取消UIGestureRecognizerStateFailed 失敗UIGestureRecognizerStateRecognized
與 UIGestureRecognizerStateEnded 相同
104
105
取消 Auto Layout取消 Auto Layout
106
Tap ( 點擊 )按一下跳至下一層
點擊次數 手指數
107
Tap ( 點擊 )透過 Segue 直接連到 Swipe View Controller
108
Swipe (掃動 )
透過左右滑動回到上一層或下一層
109
連接 Tap 與 Swipe
加入 Bar Button Item 並建立 Segue
110
選擇 Push
111
建立的 Segue
112
113
Bar Item
設定名稱
114
Swipe (掃動 )
右到左,透過 Segue 移動 左到右,透過 Sent Action 處理
115
SwipeViewController.h
// 處理 Swipe Gesture 由左滑到右邊事件- (IBAction)handSwipeLeftToRight:(id)sender {
// pop 回上一層 view controller [self.navigationController popViewControllerAnimated:TRUE];
}
116
Pan (拖拉 )
透過左右滑動回到上一層或下一層
117
Pan (拖拉 )
由 Sent Action 處理,藉由 x 方向的移動速度,判斷左右
118
PanViewController.h// 處理 Pan 手勢- (IBAction)handlePan:(id)sender {
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer*)sender;
// 判斷是否為 pan 手勢結束狀態 if (panGesture.state == UIGestureRecognizerStateEnded)
{
// 取得移動速度 CGPoint velocity = [panGesture velocityInView:self.view];
// x > 0 為 左向右滑動, pop 回上一層 view controller if(velocity.x > 0)
{
[self.navigationController popViewControllerAnimated:TRUE];
}
// x < 為 右向左滑動 , 執行 segueLongPress // push 下一層 view controller (Long Press) else
{
[self performSegueWithIdentifier:@"segueLongPress" sender:self];
}
}
}
119
Long Press (長按 )長按切換按鈕顏色
120
Long Press (長按 )
由 Sent Action 處理
121
LongViewController.h@interface LongPressViewController : UIViewController
// 處理長按手勢- (IBAction)handleLongPress:(id)sender;
// 中央按鈕@property (weak, nonatomic) IBOutlet UIButton *longPressButton;
@end
LongViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// 設定角落的圓弧半徑為 60 self.longPressButton.layer.cornerRadius = 60;
// 設定邊界線條寬度為 0,即隱藏邊界 self.longPressButton.layer.borderWidth = 0.0f;
}
122
LongViewController.m
// 處理長按手勢- (IBAction)handleLongPress:(id)sender
{
// 型態轉換 UILongPressGestureRecognizer *longPressGesture = (UILongPressGestureRecognizer*)sender;
// 長按手勢開始 if (longPressGesture.state == UIGestureRecognizerStateBegan)
{
// 按鈕綠色變紅色 if ([self.longPressButton.backgroundColor isEqual:[UIColor greenColor]])
{
self.longPressButton.backgroundColor = [UIColor redColor];
}
// 按鈕紅色變綠色 else
{
self.longPressButton.backgroundColor = [UIColor greenColor];
}
}
}
123
Pinch (縮放 )利用 Pinch 縮放圖片大
小
124
UIImageView
125
Pinch (縮放 )
由 Sent Action 處理
126
PinchViewController.m
@interface PinchViewController ()
{
// 記錄上一次放大的倍數 CGFloat lastScale;
}
@end
127
PinchViewController.m
// 處理 pinch 手勢- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer
{
float scale;
// 若為手勢剛開始狀態,設定 lastScale = 1.0 (無放大縮小 )
if (recognizer.state == UIGestureRecognizerStateBegan)
{
lastScale = 1.0;
return;
}
// 其它狀態 , 依 lastScale 的值來決定要放大多少 scale = 1 + (recognizer.scale-lastScale);
lastScale = recognizer.scale;
// 放大圖片 self.imageView.transform = CGAffineTransformScale(self.imageView.transform, scale, scale);
}
128
Rotate (旋轉 ) 旋轉圖片
129
Rotate (旋轉 )
由 Sent Action 處理
130
RotationViewController.m
@interface RotationViewController ()
{
// 記錄上次旋轉角度 CGFloat lastRotation;
}
@end
131
RotationViewController.m
// 處理 Rotation 手勢- (IBAction)handleRotation:(UIRotationGestureRecognizer *)sender
{
// 若為手勢剛開始狀態,設定 lastRotation = 0.0 (無旋轉 )
if([sender state] == UIGestureRecognizerStateBegan) {
lastRotation = 0.0;
return;
}
// 其它狀態 , 依 lastRotation 的值及來決定要旋轉幾度 CGFloat rotation = 0.0 - (lastRotation - [sender rotation]);
// 計算新的 transform CGAffineTransform currentTransform = self.imageView.transform;
CGAffineTransform newTransform = CGAffineTransformRotate(currentTransform,rotation);
// 設定新的 transform [self.imageView setTransform:newTransform];
// 更新 lastRotation 下次使用 lastRotation = [sender rotation];
}
132
結合 Pan, Pinch, Rotation
可移動、縮放、旋轉圖片
133
PinchRotationViewController.m
@interface PinchRotationViewController : UIViewController
// 處理 Rotation 手勢- (IBAction)handleRotation:(UIRotationGestureRecognizer *)sender;
// 處理 pinch 手勢- (IBAction)handlePinch:(UIPinchGestureRecognizer *)sender;
// 處理 Pan 手勢- (IBAction)handlePan:(UIPanGestureRecognizer *)sender;
// UIImageView, 圖片 UI 物件@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
134
PinchRotationViewController.m
// 處理 pan 手勢- (IBAction)handlePan:(UIPanGestureRecognizer *)sender
{
// 若是 pan 手勢進行中或結束時,更新圖片位置 if ((sender.state == UIGestureRecognizerStateChanged) ||
(sender.state == UIGestureRecognizerStateEnded)) {
// 取得手勢位置 CGPoint location = [sender locationInView:self.view];
// 設定圖片中心 self.imageView.center = location;
}
}
135
Collection
136
UICollectionViewController
137
Collection 設計
138
Collection Story Board
139
Collection View
勾選 Header把 UIImageView拉到 Cell
140
141
設定 Cell 及 Header 大小
142
嵌入 Navigation View Controller
Embedded View Controller
143
加入 Full Image View Controller
144
建立 Section Header 的類別及識別
建立 Cell 的類別及識別
建立 Full Image View Controller 類別
145
Collection Reusable View
設定 Identifier
146
Collection Reusable View
設定 Identifier
147
Full Image View Controller UIImageView 設定
148
MyCollectionViewCell.h
@interface MyCollectionViewCell : UICollectionViewCell
@property (strong, nonatomic) IBOutlet UIImageView *imageView;
@end
MySupplementaryView.h
@interface MySupplementaryView : UICollectionReusableView
@property (weak, nonatomic) IBOutlet UILabel *headerLabel;
@end
149
FullImageViewController.h
@interface FullImageViewController : UIViewController
// 要顯示的 UIImageView@property (weak, nonatomic) IBOutlet UIImageView *imageView;
// 暫存的圖片@property (strong, nonatomic) UIImage *image;
@end
FullImageViewController.m
- (void)viewWillAppear:(BOOL)animated
{
// 設定 imageView 的圖片 self.imageView.image = self.image;
}
150
MyCollectionViewController.m
#import "MySupplementaryView.h"
#import "FullImageViewController.h"
@interface MyCollectionViewController ()
{
// 選擇的圖片 UIImage *selectedImage;
// Secion 0 圖片名稱字串陣列 NSMutableArray *images0;
// Secion 1 圖片名稱字串陣列 NSMutableArray *images1;
}
@end
151
MyCollectionViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// 建立 2 個 Section 的圖片名稱字串陣列 images0 = [@[@"fish1.png",
@"fish2.png",
@"fish3.png",] mutableCopy];
images1 = [@[@"fish3.png",
@"fish2.png",
@"fish1.png",] mutableCopy];
}
- (void)viewWillAppear:(BOOL)animated
{
// 隱藏 Navigation Bar [[self navigationController] setNavigationBarHidden:YES animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated
{
// 顯示 Navigation Bar [[self navigationController] setNavigationBarHidden:NO animated:YES];
}
152
MyCollectionViewController.m
// 回傳 Section 個數 , 2 個 Section- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 2;
}
// 回傳 Section 裡的項目個數- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
if (section == 0)
return images0.count;
return images1.count;
}
UICollectionViewDataSource
UICollectionViewDataSource
153
MyCollectionViewController.m
// 設定各 Section 的名稱-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
MySupplementaryView *header = nil;
// 判斷是 header 還是 footer if ([kind isEqual:UICollectionElementKindSectionHeader])
{
// 取得 header header = [collectionView dequeueReusableSupplementaryViewOfKind:kind
withReuseIdentifier:@"MyHeader"
forIndexPath:indexPath];
// 設定標題 if (indexPath.section == 0)
header.headerLabel.text = @"Fish 0 Gallery";
else
header.headerLabel.text = @"Fish 1 Gallery";
}
return header;
}
UICollectionViewDataSource
154
MyCollectionViewController.m
// 設定要顯示 cell- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UIImage *image;
// 取得 cell MyCollectionViewCell *myCell = [collectionView
dequeueReusableCellWithReuseIdentifier:@"MyCell"
forIndexPath:indexPath];
// 依 cell 位置,建立圖片 if (indexPath.section == 0)
image = [UIImage imageNamed:images0[indexPath.row]];
else
image = [UIImage imageNamed:images1[indexPath.row]];
// 設定 cell 要顯示的圖片 myCell.imageView.image = image;
return myCell;
}
UICollectionViewDataSource
155
MyCollectionViewController.m
// 處理選擇的項目- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
// 取得選擇的圖片 if (indexPath.section == 0)
selectedImage = [UIImage imageNamed:images0[indexPath.row]];
else
selectedImage = [UIImage imageNamed:images1[indexPath.row]];
// 執行 Segue: pushShowFullImage [self performSegueWithIdentifier: @"pushShowFullImage" sender: self];
}
// 準備執行 Segue- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// 判斷要執行的 Segue 是否為 pushShowFullImage if ([[segue identifier] isEqualToString:@"pushShowFullImage"])
{
// 取得目標 View Controller FullImageViewController *fvc = [segue destinationViewController];
// 設定 FullImageViewController 要顯示的圖片 fvc.image = selectedImage;
}
}
UICollectionViewDelegate
156
Timer and Thread
157
Clock
使用 NSTimer 及 NSThread 實作時鐘
利用 UISwitch 切換開關
使用 PickerView 選擇時間格式
158
設定 UISwitch 的 Value Changed 事件
159
設定 Picker View 的 delegate 及 datasource
160
NSThread 限制透過 NSThread 建立出來的執行緒與控制界面的主執行緒是不一樣的
NSTimer 建立出來的計時器可以變更界面內容
NSThread 逼立出來的執行緒不能變更界面內容需要回到主執行緒才能變更
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
161
取得介面元件的另一個方法介面元件都有一個 Tag
不一定要透過 Story Board 連接 View Controller 與介面元件之間的關係
View Controller 可以透過 Tag 在程式執行期間,動態取得介面元件
[self.view viewWithTag:1]
162
設定 Tag
163
ClockViewController.m
@interface ClockViewController ()
{
// UI 界面元件 UILabel *clockLabel;
UISwitch *timerSwitch;
UISwitch *threadSwitch;
// 儲存 date format 字串的陣列 NSArray *dateFormat;
// 計時器 NSTimer *clockTimer;
// 執行緒 NSThread *clockThread;
// 執行緒生命判斷標籤 BOOL isThreadAlive;
// 目前使用的 date format NSDateFormatter *dateFormatter;
}
@end
164
ClockViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
clockTimer = nil;
clockThread = nil;
isThreadAlive = FALSE;
// 透過 tag 取得 UI 介面元件 clockLabel = (UILabel*)[self.view viewWithTag:1];
timerSwitch = (UISwitch*)[self.view viewWithTag:2];
threadSwitch = (UISwitch*)[self.view viewWithTag:3];
// 設定 dateFormat 陣列 dateFormat = [NSArray arrayWithObjects:@"HH:mm:ss", @"ss", @"yyyy-MM-dd HH:mm:ss", @"MM/dd HH:mm:ss", nil];
// 設定預設 date format 為 dateFormat 字串陣列裡的第 0 個 dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:[dateFormat objectAtIndex:0]];
}
165
ClockViewController.m
// 設定 PickerView 的元件個數- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}
// 設定 PickerView 元件裡的列數- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return dateFormat.count;
}
// 設定 PickerView 列要顯示的字串- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
return [dateFormat objectAtIndex:row];
}
// 處理選擇 PickerViw 列的事件- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
// 設定新的 date format [dateFormatter setDateFormat:[dateFormat objectAtIndex:row]];
}
UIPickerViewDataSource
UIPickerViewDataSource
UIPickerViewDataSource
UIPickerViewDelegate
166
ClockViewController.m
// 啟動 Timer- (void)startTimer
{
if (clockTimer == nil)
{
clockTimer =
[NSTimer scheduledTimerWithTimeInterval:1 target:self
selector:@selector(clockTimeout) userInfo:nil repeats:YES];
}
}
// 停止 Timer- (void)stopTimer
{
if (clockTimer != nil)
{
[clockTimer invalidate];
clockTimer = nil;
}
}
// 處理 timeout 事件 , 更新要顯示的時間- (void)clockTimeout
{
// 依目前的 dateFormatter 格式顯示時間 clockLabel.text = [dateFormatter stringFromDate:[NSDate date]];
}
167
ClockViewController.m
// 啟動執行緒- (void)startThread
{
if (clockThread == nil)
{
clockThread = [[NSThread alloc] initWithTarget:self
selector:@selector(threadMain)
object:nil];
isThreadAlive = TRUE;
[clockThread start]; // Actually create the thread
}
}
// 停止執行緒- (void)stopThread
{
if (clockThread != nil)
{
clockThread = nil;
isThreadAlive = FALSE;
}
}
168
ClockViewController.m
// 執行緒本體- (void)threadMain
{
while(isThreadAlive)
{
// 通知 Main Thread 執行 clockTimeout [self performSelectorOnMainThread:@selector(clockTimeout) withObject:nil waitUntilDone:FALSE];
// 睡 1 秒 sleep(1);
}
}
169
ClockViewController.m// 處理 UISwitch value changed 事件- (IBAction)handleSwitchValueChanged:(id)sender { // 若是 timerSwitch // 1. 停止 thread 執行 // 2. 依 timerSwitch 狀態決定要執行或是停止 timer if (sender == timerSwitch) { [self stopThread]; threadSwitch.on = FALSE; if (timerSwitch.on) [self startTimer]; else [self stopTimer]; } // 若是 threadSwitch // 1. 停止 timer 執行 // 2. 依 threadSwitch 狀態決定要執行或是停止 thread else { [self stopTimer]; timerSwitch.on = FALSE; if (threadSwitch.on) [self startThread]; else [self stopThread]; }}
UISwitch
170
iAd 與 IAP
171
iAd 與 IAPSimulator 不能測試 IAP
需要購買 iOS Developer Program
賺錢全靠他們倆
建立 IAP 項目
建立測試人員不然你要一直花自己的錢去測試 IAP 嗎?
172
iOS Developer Program
173
建立新的 App
iTune Connecthttps://itunesconnect.apple.com
174
175
176
上架時間及價格
177
Version Information
178
179
App 資料
180
聯絡資訊
181
上傳 App 圖片
182
新增 IAP
183
新增 IAP
184
選擇單賣的方式
185
IAP 項目名稱及價格
186
設定 IAP 項目語言
187
新增測試人員
188
新增測試人員
189
新增測試人員Email 可以亂填沒關係,不會認證,只會當 ID 用
190
問題如何取得 IAP 項目資料?
如何購買 IAP 項目?
如何取得已購買項目?
如何記録已購買項目?
iAd 廣告
192
193
設定按鈕 Touch Up Inside 事件處理
194
iAd 及 IAP framework加入 iAd.framework 及 StoreKit.framework
195
處理 IAP項目 說明
取得 IAP 項目
透過 SKProductRequest 處理設定 SKProductRequestDelegate實作 productsRequest:didReceiveResponse:啟動 SKProductRequest 物件
購買 IAP 項目
透過 [SKPaymentQueue defaultQueue] 處理實作 paymentQueue:updatedTransactions:設定 addTransactionObserver[[SKPaymentQueue defaultQueue] addPayment:payment]
恢復 IAp 項目
透過 [SKPaymentQueue defaultQueue] 處理設定 addTransactionObserver實作 paymentQueue:updatedTransactions:[[SKPaymentQueue defaultQueue] restoreCompletedTransactions]
196
取得 IAP 項目清單
讀取 User Default更新 IAP 項目界面
啟動 App
處理 SKPaymentQueue
交易事件
購買
SKPaymentQueue addPayment
記錄購買項目在 User Default
處理 SKPaymentQueue
交易事件
恢復購買
SKPaymentQueue restoreCompleted
Transactions
記錄購買項目在 User Default
197
使用者設定檔 使用 NSUserDefaults 類別 儲存在
<Application_Home>/Library/Preferences/<BundleId>.plist 以 Key, Value 的方式儲存 Value 資料型態
Integer, float, double, string, dictionary, object
// 取得 NSUserDefault 物件NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 設定 (key, value)[defaults setBool:FALSE forKey:@"key"];
// 依 key 取得 valueBOOL value = [defaults boolForKey:@"key"];
198
IADViewController.h
#import <UIKit/UIKit.h>
#import <iAd/iAd.h>
#import <StoreKit/StoreKit.h>
@interface IADViewController : UIViewController<SKProductsRequestDelegate, SKPaymentTransactionObserver>
// IAP 項目 UI 介面元件@property (weak, nonatomic) IBOutlet UILabel *productIdentifierLabel;
@property (weak, nonatomic) IBOutlet UILabel *productPriceLabel;
@property (weak, nonatomic) IBOutlet UILabel *productDescriptionLabel;
@property (weak, nonatomic) IBOutlet UILabel *productTitleLabel;
@property (weak, nonatomic) IBOutlet UILabel *purchaseStatus;
// 廣告@property (weak, nonatomic) IBOutlet ADBannerView *adBanner;
// 按下購買- (IBAction)pressPurchase:(id)sender;
// 按下恢復購買狀態- (IBAction)pressRestore:(id)sender;
@end
199
IADViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// 清除購買記錄 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:FALSE forKey:PRODUCT_IDENTIFIER];
// 更新購買狀態 [self updatePurchaseStatus];
// 下載 IAP Product [self retrieveIapProduct];
// 註冊 SKPaymentQueue 觀察者 [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
200
IADViewController.m
// 按下購買按鈕- (IBAction)pressPurchase:(id)sender {
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
// 按下恢復購買按鈕- (IBAction)pressRestore:(id)sender {
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
// 設定 IAP 項目已購買- (void)updateIAPPurchased:(NSString*)key
{
// 取得 UserDefault 物件 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 設定 com.comig.iphoneui.iap.noad 的值為 TRUE [defaults setBool:TRUE forKey:key];
[self updatePurchaseStatus];
}
201
IADViewController.m
// 更新介面購買狀態-(void)updatePurchaseStatus
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:PRODUCT_IDENTIFIER])
{
self.purchaseStatus.text = @"已購買 ";
// 隱藏廣告 self.adBanner.hidden = YES;
}
else
{
self.purchaseStatus.text = @"尚未購買 ";
// 顯示廣告 self.adBanner.hidden = NO;
}
}
202
IADViewController.m
// 開始下載 IAP 項目-(void)retrieveIapProduct {
// 設定要下載的項目 _productIdentifiers = [NSSet setWithObjects:PRODUCT_IDENTIFIER, nil];
// 建立 SKProductRequest 物件 _productsRequest = [[SKProductsRequest alloc]
initWithProductIdentifiers:_productIdentifiers];
// 設定委派 _productsRequest.delegate = self;
[_productsRequest start];
}
// 處理下載 IAP 項目- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
// 取得第一個 IAP 項目 , 注意 , response.products 是陣列 product = [response.products objectAtIndex:0];
self.productTitleLabel.text = product.localizedTitle;
self.productIdentifierLabel.text = product.productIdentifier;
self.productPriceLabel.text = product.localizedPrice;
self.productDescriptionLabel.text = product.localizedDescription;
}
203
處理 SKPaymentQueue 交易事件
項目 說明SKPaymentTransactionStatePurchasing 購買處理中SKPaymentTransactionStatePurchased 購買成功SKPaymentTransactionStateFailed 購買 /恢復失敗SKPaymentTransactionStateRestored 恢復成功
enum SKPaymentTransactionState
204
IADViewController.m
// 處理 SKPaymentQueue 交易事件- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
[self alertMessage:@" 購買成功 "];
[self updateIAPPurchased:transaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self alertMessage:@" 購買失敗 "];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self alertMessage:@"恢復已購買成功 "];
[self updateIAPPurchased:transaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
default:
break;
}
};
}
205
整合所有範例
206
統整 Story Board
Main
Tab BarPage
ControlTable
Collection
Clock iAd
207
版面設計
208
設定 StoryBoard ID View Controller 需要設定
StoryBoardID
因為 Main StoryBoard 已經包了一個 Navigation View Controller ,所以不能以 Navigation View Controller 當開頭
209
ViewController.m@interface ViewController (){ UIStoryboard *tabBarStoryboard; UIStoryboard *pageControlStoryboard; UIStoryboard *tableStoryboard; UIStoryboard *touchStoryboard; UIStoryboard *collectionStoryboard; UIStoryboard *clockStoryboard; UIStoryboard *iADStoryboard;}@end
@implementation ViewController
- (void)viewDidLoad{ [super viewDidLoad]; // 建立 Story Bard 物件 tabBarStoryboard = [UIStoryboard storyboardWithName:@"TabBar" bundle:nil]; pageControlStoryboard = [UIStoryboard storyboardWithName:@"PageControl" bundle:nil]; tableStoryboard = [UIStoryboard storyboardWithName:@"Table" bundle:nil]; touchStoryboard = [UIStoryboard storyboardWithName:@"Touch" bundle:nil]; collectionStoryboard = [UIStoryboard storyboardWithName:@"Collection" bundle:nil]; clockStoryboard = [UIStoryboard storyboardWithName:@"Clock" bundle:nil]; iADStoryboard = [UIStoryboard storyboardWithName:@"IAD" bundle:nil];}
210
ViewController.m
// 顯示 Tab Bar 範例- (IBAction)pressTabBar:(id)sender {
UIViewController *vc = [tabBarStoryboard instantiateViewControllerWithIdentifier:@"TabBarViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
// 顯示 Page Control 範例- (IBAction)pressPageControl:(id)sender {
UIViewController *vc = [pageControlStoryboard instantiateViewControllerWithIdentifier:@"PageViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
// 顯示 Table 範例- (IBAction)pressTable:(id)sender {
UIViewController *vc = [tableStoryboard instantiateViewControllerWithIdentifier:@"TableViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
// 顯示 Touch 範例- (IBAction)pressTouch:(id)sender {
UIViewController *vc = [touchStoryboard instantiateViewControllerWithIdentifier:@"TapViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
211
ViewController.m
// 顯示 Collection 範例- (IBAction)pressCollection:(id)sender {
UIViewController *vc = [collectionStoryboard instantiateViewControllerWithIdentifier:@"CollectionViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
// 顯示 Timer 與 Thread 範例- (IBAction)pressTimerAndThread:(id)sender {
UIViewController *vc = [clockStoryboard instantiateViewControllerWithIdentifier:@"ClockViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
// 顯示 iAd 與 IAP 範例- (IBAction)pressIADAndIAP:(id)sender {
UIViewController *vc = [iADStoryboard instantiateViewControllerWithIdentifier:@"IADViewController"];
[self.navigationController pushViewController:vc animated:TRUE];
}
212
謝謝