2015/10/28數組
Day 27app
今天學習了即時通信應用的UI佈局,只是簡單的利用UITableView展現數據框架
第一步ide
先利用storyboard把頁面的框架搭起來佈局
顯示的數據是存在plist文件裏的,因此要把他們轉成模型post
typedef enum {學習
YUMessageTypeMe = 0, // 本身測試
YUMessageTypeOther // 其餘人動畫
} YUMessageType;atom
@interface YUMessage : NSObject
@property (nonatomic, copy) NSString *text;
@property (nonatomic, copy) NSString *time;
@property (nonatomic, assign) YUMessageType type;
@property (nonatomic, assign) BOOL hiddenTime;
- (instancetype)initWithDic:(NSDictionary *)dic;
+ (instancetype)messageWithDic:(NSDictionary *)dic;
@end
因爲本身發的消息與別人的消息顯示位置不一樣,用了枚舉來區別,另外hiddenTime是爲了避免重複顯示相同的時間。
每一個消息都是顯示在一個UITableViewCell上的,由於隨着數據的不一樣,文字的長短有區別,也就致使每一個cell的高度可能不同,這樣的狀況相似於前面的作的微博頁面,解決方法就是再封裝一個frame的模型(實體類),在裏面計算出子控件的frame和cell的高度
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class YUMessage;
@interface YUMessageFrame : NSObject
@property (nonatomic, assign, readonly) CGRect iconF;
@property (nonatomic, assign, readonly) CGRect textF;
@property (nonatomic, assign, readonly) CGRect timeF;
@property (nonatomic, assign, readonly) CGFloat cellHight;
@property (nonatomic, strong) YUMessage *message;
@end
在setMessage方法裏設置各個frame和cellHeight
- (void)setMessage:(YUMessage *)message {
_message = message;
CGFloat padding = 15;
//時間
if (message.hiddenTime == NO) {
CGFloat timeW = [UIScreen mainScreen].bounds.size.width;
CGFloat timeH = 40;
_timeF = CGRectMake(0, 0, timeW, timeH);
}
//頭像
CGFloat iconX;
CGFloat iconY = CGRectGetMaxY(_timeF) + padding;
CGFloat iconW = 50;
CGFloat iconH = 50;
//消息內容
CGFloat textX;
CGFloat textY = iconY;
CGFloat textMaxW = 200;
CGSize textSize = [message.text boundingRectWithSize:CGSizeMake(textMaxW, MAXFLOAT) options:NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:16]} context:nil].size;
CGFloat textW = textSize.width + 40;
CGFloat textH = textSize.height + 30;
if (message.type == YUMessageTypeMe) {
iconX = [UIScreen mainScreen].bounds.size.width - padding - iconW;
textX = iconX - padding - textW;
} else {
iconX = padding;
textX = iconX + iconW + padding;
}
_iconF = CGRectMake(iconX, iconY, iconW, iconH);
_textF = CGRectMake(textX, textY, textW, textH);
_cellHight = MAX(CGRectGetMaxY(_iconF), CGRectGetMaxY(_textF)) + padding;
}
一個個排好就行,值得注意的是,本身的消息在右,別人的消息在左
第二步
ViewController里加載數據,實現數據源方法
在屬性的getter方法裏懶加載數據
- (NSMutableArray *)messageFrames {
if (_messageFrames == nil) {
NSMutableArray *result = [[NSMutableArray alloc] init];
NSString *path = [[NSBundle mainBundle] pathForResource:@"messages.plist" ofType:nil];
NSArray *dics = [NSArray arrayWithContentsOfFile:path];
for (NSDictionary *dic in dics) {
YUMessage *message = [YUMessage messageWithDic:dic];
YUMessageFrame *lastF = [result lastObject];
YUMessageFrame *messageFrame = [[YUMessageFrame alloc] init];
messageFrame.message = message;
//消息的時間相同就隱藏時間
messageFrame.message.hiddenTime = [lastF.message.time isEqualToString:message.time];
[result addObject:messageFrame];
}
_messageFrames = result;
}
return _messageFrames;
}
#pragma mark - 數據源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.messageFrames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
YUMessageCell *cell = [YUMessageCell cellWithTableView:tableView];
cell.messageFrame = self.messageFrames[indexPath.row];
return cell;
}
因爲系統提供的cell根本不夠我顯示數據,因而自定義cell,YUMessageCell爲我自定義cell的類名。cell的建立封裝在本身的類方法裏,這樣即實現瞭解耦,也簡化了ViewController的代碼。
在類擴展裏包含子控件
@interface YUMessageCell()
@property (nonatomic, weak) UIImageView *iconView;
@property (nonatomic, weak) UILabel *timeView;
@property (nonatomic, weak) UIButton *textView;
@end
在初始化方法里加載子控件
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
UIImageView *iconView = [[UIImageView alloc] init];
iconView.layer.cornerRadius = 8;
//iconView.clipsToBounds = YES;
[self.contentView addSubview:iconView];
self.iconView = iconView;
UILabel *timeView = [[UILabel alloc] init];
timeView.textAlignment = NSTextAlignmentCenter;
timeView.font = [UIFont systemFontOfSize:12];
[self.contentView addSubview:timeView];
self.timeView = timeView;
UIButton *textView = [[UIButton alloc] init];
textView.titleLabel.font = [UIFont systemFontOfSize:16];
textView.titleLabel.numberOfLines = 0;//自動換行
[self.contentView addSubview:textView];
self.textView = textView;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
在setter方法裏設置數據子控件的數據與frame
- (void)setMessageFrame:(YUMessageFrame *)messageFrame {
_messageFrame = messageFrame;
YUMessage *msg = messageFrame.message;
if (msg.hiddenTime) {
self.timeView.hidden = YES;
} else {
self.timeView.hidden = NO;
self.timeView.frame = messageFrame.timeF;
self.timeView.text = msg.time;
}
self.iconView.frame = messageFrame.iconF;
self.iconView.image = [UIImage imageNamed:(msg.type == YUMessageTypeMe)?@"me":@"other"];
self.textView.frame = messageFrame.textF;
[self.textView setTitle:msg.text forState:UIControlStateNormal];
// 4.設置聊天背景
NSString *normal, *high;
if (msg.type == YUMessageTypeOther) {
normal = @"chat_recive_nor";
high = @"chat_recive_press_pic";
[self.textView setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
} else {
normal = @"chat_send_nor";
high = @"chat_send_press_pic";
[self.textView setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
}
//設置按鈕內部的內間距
self.textView.contentEdgeInsets = UIEdgeInsetsMake(15, 20, 15, 20);
// 拉伸圖片 且保留圖片周圍的像素不被拉伸
UIImage *normalBackground = [UIImage imageNamed:normal];
normalBackground = [normalBackground stretchableImageWithLeftCapWidth:normalBackground.size.width * 0.5 topCapHeight:normalBackground.size.height * 0.5];
//normalBackground = [normalBackground resizableImageWithCapInsets:UIEdgeInsetsMake(15, 20, 15, 20) resizingMode:UIImageResizingModeStretch];
[self.textView setBackgroundImage:normalBackground forState:UIControlStateNormal];
}
因爲,聊天的背景圖片是這樣的
若是直接放進去,被拉伸後的效果很坑爹
因而須要方法拉伸圖片的時候四周不跟着拉伸,我的認爲比較好記又好用的方法是UIImage的一個對象方法,
- (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight __TVOS_PROHIBITED;
這兩個參數通常傳入,圖片寬度的一半和高度的一半,這個方法的原理是按照你給的參數,在那個座標取像素爲1的正方形進行拉伸,這樣就能夠保持圖片的周邊在拉伸的時候像素不變了,效果以下
最後還有個問題,就是點擊文本框的時候鍵盤彈出,就會蓋住下方的部分,因此須要監聽鍵盤的狀態,在鍵盤出來的時候View要隨着鍵盤一塊兒動。那麼問題又來了,怎麼監聽鍵盤呢?
這就要靠通知機制了
一個完整的通知通常包含3個屬性:
初始化一個通知(NSNotification)對象
通知中心(NSNotificationCenter)提供了相應的方法來幫助發佈通知
通知中心(NSNotificationCenter)提供了方法來註冊一個監聽通知的監聽器(Observer)
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject;
observer:監聽器,即誰要接收這個通知
aSelector:收到通知後,回調監聽器的這個方法,而且把通知對象當作參數傳入
aName:通知的名稱。若是爲nil,那麼不管通知的名稱是什麼,監聽器都能收到這個通知
anObject:通知發佈者。若是爲anObject和aName都爲nil,監聽器都收到全部的通知
- (id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
name:通知的名稱
obj:通知發佈者
block:收到對應的通知時,會回調這個block
queue:決定了block在哪一個操做隊列中執行,若是傳nil,默認在當前操做隊列中同步執行
通知中心不會保留(retain)監聽器對象,在通知中心註冊過的對象,必須在該對象釋放前取消註冊。不然,當相應的通知再次出現時,通知中心仍然會向該監聽器發送消息。由於相應的監聽器對象已經被釋放了,因此可能會致使應用崩潰
通知中心提供了相應的方法來取消註冊監聽器
通常在監聽器銷燬以前取消註冊(如在監聽器中加入下列代碼):
- (void)dealloc {
//[super dealloc]; 非ARC中須要調用此句
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
UIDevice類提供了一個單例對象,它表明着設備,經過它能夠得到一些設備相關的信息,好比電池電量值(batteryLevel)、電池狀態(batteryState)、設備的類型(model,好比iPod、iPhone等)、設備的系統(systemVersion)
經過[UIDevice currentDevice]能夠獲取這個單粒對象
UIDevice對象會不間斷地發佈一些通知,下列是UIDevice對象所發佈通知的名稱常量:
➢ UIDeviceOrientationDidChangeNotification // 設備旋轉
➢ UIDeviceBatteryStateDidChangeNotification // 電池狀態改變
➢ UIDeviceBatteryLevelDidChangeNotification // 電池電量改變
➢ UIDeviceProximityStateDidChangeNotification // 近距離傳感器(好比設備貼近了使用者的臉部)
咱們常常須要在鍵盤彈出或者隱藏的時候作一些特定的操做,所以須要監聽鍵盤的狀態
鍵盤狀態改變的時候,系統會發出一些特定的通知
➢ UIKeyboardWillShowNotification // 鍵盤即將顯示
➢ UIKeyboardDidShowNotification // 鍵盤顯示完畢
➢ UIKeyboardWillHideNotification // 鍵盤即將隱藏
➢ UIKeyboardDidHideNotification // 鍵盤隱藏完畢
➢ UIKeyboardWillChangeFrameNotification // 鍵盤的位置尺寸即將發生改變
➢ UIKeyboardDidChangeFrameNotification // 鍵盤的位置尺寸改變完畢
系統發出鍵盤通知時,會附帶一下跟鍵盤有關的額外信息(字典),字典常見的key以下:
➢ UIKeyboardFrameBeginUserInfoKey // 鍵盤剛開始的frame
➢ UIKeyboardFrameEndUserInfoKey // 鍵盤最終的frame(動畫執行完畢後)
➢ UIKeyboardAnimationDurationUserInfoKey // 鍵盤動畫的時間
➢ UIKeyboardAnimationCurveUserInfoKey // 鍵盤動畫的執行節奏(快慢)
我測試打印出來的信息以下(用的6s模擬器)
UIKeyboardAnimationCurveUserInfoKey = 7;
UIKeyboardAnimationDurationUserInfoKey = "0.25";
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {375, 258}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {187.5, 796}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {187.5, 538}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 667}, {375, 258}}";
UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 409}, {375, 258}}";
UIKeyboardIsLocalUserInfoKey = 1;
好了,知道這些我就能夠監聽鍵盤了,讓鍵盤彈出的同時整個view也向上移動就好了,鍵盤收起也是同樣,跟着向下移動
首先監聽鍵盤通知,通常在viewDidLoad方法裏寫
//監聽鍵盤的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardChange:) name:UIKeyboardWillChangeFrameNotification object:nil];
實現方法
- (void)keyboardChange:(NSNotification *)note {
CGFloat duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect keyboardFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat transfromY = (CGFloat)(keyboardFrame.origin.y - self.view.frame.size.height);
[UIView animateWithDuration:duration animations:^{
self.view.transform = CGAffineTransformMakeTranslation(0, transfromY);
}];
}
利用transform能夠同時監聽鍵盤的彈出和收起,動畫效果使用block簡單方便
好啦!大功告成,總結一下,繼續熟悉了UITableView的使用,以及自定義Cell,另外學到了通知機制。
2015/10/30
Day 28
今天作了一個QQ好友列表展現,先放效果圖
仍是利用UITableView,這回是直接在starboard裏拖入一個UITableViewController,這個UITableViewController自動實現數據源方法和代理方法,不須要我手寫了
老規矩,先封裝模型類
這個算是多維數組了,每一個friend會在一個cell中顯示,最好分兩個模型封裝,group和friend
@interface YUFriend : NSObject
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *intro;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign, getter=isVip) BOOL vip;
+ (instancetype)friendWithDic:(NSDictionary *)dic;
- (instancetype)initWithDic:(NSDictionary *)dic;
@end
bool類型的數據getter方法最好用is開頭
@interface YUGroup : NSObject
@property (nonatomic, strong) NSArray *friends;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int online;
@property (nonatomic, assign) BOOL open;
+ (instancetype)groupWithDic:(NSDictionary *)dic;
- (instancetype)initWithDic:(NSDictionary *)dic;
@end
open表示組的打開或關閉
另外group的init方法中,須要把字典數組轉化成對象數組
- (instancetype)initWithDic:(NSDictionary *)dic {
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dic];
NSMutableArray *friends = [NSMutableArray array];
for (NSDictionary *dic in self.friends) {
YUFriend *friend = [YUFriend friendWithDic:dic];
[friends addObject:friend];
}
self.friends = friends;
}
return self;
}
而後加載plist數據,實現ViewController的數據源方法
因爲這是分組的,numberOfSectionsInTableView終於用上了。
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.groups.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
YUGroup *group = self.groups[section];
return group.open ? group.friends.count : 0;
}
自定義cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
YUFriendCell *cell = [YUFriendCell cellWithTableView:tableView];
YUGroup *group = self.groups[indexPath.section];
cell.friendData = group.friends[indexPath.row];
return cell;
}
另外,每組的header也須要自定義(delegate屬性後面收縮組的時候要用到)
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
YUGroupHeader *header = [YUGroupHeader headerWithTableView:tableView];
header.delegate = self;
header.group = self.groups[section];
return header;
}
自定義cell
@interface YUFriendCell : UITableViewCell
@property (nonatomic, strong) YUFriend *friendData;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@end
setter方法加載數據
- (void)setFriendData:(YUFriend *)friendData {
_friendData = friendData;
self.imageView.image = [UIImage imageNamed:friendData.icon];
self.textLabel.text = friendData.name;
self.textLabel.textColor = friendData.isVip ? [UIColor redColor] : [UIColor blackColor];
self.detailTextLabel.text = friendData.intro;
}
init方法,作些基礎的設置
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.textLabel.font = [UIFont boldSystemFontOfSize:15];
}
return self;
}
初始化類方法,方便控制器使用
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *yu3 = @"friends";
YUFriendCell *cell = [tableView dequeueReusableCellWithIdentifier:yu3];
if (cell == nil) {
cell = [[YUFriendCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:yu3];
}
return cell;
}
自定義headerView
@interface YUGroupHeader : UITableViewHeaderFooterView
@property (nonatomic, strong) YUGroup *group;
+ (instancetype)headerWithTableView:(UITableView *)tableView;
@end
有兩個子控件,左邊的按鈕和右邊的在線人數
@interface YUGroupHeader ()
@property (nonatomic, weak) UIButton *nameBtn;
@property (nonatomic, weak) UILabel *onlineLabel;
@end
init方法里加入子控件和設置子控件的數據,
不建議在這裏設置子控件的frame,可能不許
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier {
。。。
}
在這個方法裏設置必定準,另外注意調用super
- (void)layoutSubviews {
[super layoutSubviews];
self.nameBtn.frame = self.bounds;
self.onlineLabel.frame = CGRectMake(self.frame.size.width - 160, 0, 150, self.frame.size.height);
}
這樣數據就能全顯示出來,然而點擊header並不會收起來。
這就須要監聽header的點擊事件了,是誰要監聽呢?這就要看誰負責顯示數據了,是TableView,改變group的open屬性而後reloadData就能夠從新顯示數據,顯然,交給TableViewController監聽是最合適的,A的事件由B來監聽,又要用到代理模式了~
首先聲明header的代理協議
@protocol YUGroupHeaderDelegate <NSObject>
@optional
- (void)groupHeaderClick:(YUGroupHeader *)header;
@end
而後在header里加個屬性
@property (nonatomic, weak) id<YUGroupHeaderDelegate> delegate;
讓ViewController遵循協議
@interface ViewController () <YUGroupHeaderDelegate>
實現代理方法,很簡單,就是刷新數據
#pragma mark - 代理方法
- (void)groupHeaderClick:(YUGroupHeader *)header {
[self.tableView reloadData];
}
在header裏實現headerDidClick方法
- (void)headerDidClick{
self.group.open = !self.group.open;
if ([self.delegate respondsToSelector:@selector(groupHeaderClick:)]) {
[self.delegate groupHeaderClick:self];
}
}
小結:熟悉UITableViewController的使用,代理的使用
通知和代理的選擇
共同點
(好比A對象告訴B對象發生了什麼事情, A對象傳遞數據給B對象)
不一樣點
UIView自帶的方法
1> - (void)layoutSubviews;
* 當一個控件的frame發生改變的時候就會自動調用
* 通常在這裏佈局內部的子控件(設置子控件的frame)
* 必定要調用super的layoutSubviews方法
2> - (void)didMoveToSuperview;
* 當一個控件被添加到父控件中就會調用
3> - (void)willMoveToSuperview:(UIView *)newSuperview;
* 當一個控件即將被添加到父控件中會調用
2015/10/31
Day 29
今天仍是繼續UITableView,利用storyboard在cell裏拖入控件顯示數據,照例先放效果圖
第一步,在storyboard裏拖控件,不得不說,感受比以前寫的自定義cell方便多了
另外要注意你的cell的屬性設置,特別是Identifier,
數據的模型封裝類,這就不貼了
而後建立cell類
@interface YUAppCell : UITableViewCell
@property (nonatomic, strong) YUApp *app;
@end
利用app屬性來傳入數據
另外在storyboard裏設置cell 的類型爲YUAppCell,而後就能夠愉快的連線了!
像這樣,在.m文件的類擴展裏連個痛快!
而後在setter方法里加載數據
- (void)setApp:(YUApp *)app {
_app = app;
self.iconView.image = [UIImage imageNamed:app.icon];
self.nameLabel.text = app.name;
self.descLabel.text = [NSString stringWithFormat:@"大小:%@ | 下載量:%@",app.size,app.download];
}
//按鈕點擊後不可用
- (IBAction)downClick:(UIButton *)btn {
btn.enabled = NO;
}
最後在ViewController實現數據源方法,就能顯示出來了。注意cell的Identifier要跟你在storyboard裏的一致
然而坑爹的是,你在上面點了按鈕,讓按鈕不可用了,滑到下面,下面新的條目的按鈕也不可用了= =
這就是cell循環利用的一個坑。
解決方法:給cell設置數據的時候同時要把狀態也設置一下
在本例中
我在app模型里加入一個bool類型屬性,表示按鈕是否被點過了
@property (nonatomic, assign, getter=isDownloaded) BOOL downloaded;
而後點按鈕的時候,加入一行更新狀態
- (IBAction)downClick:(UIButton *)btn {
self.app.downloaded = YES;
btn.enabled = NO;
}
最後在setter方法裏設置狀態
- (void)setApp:(YUApp *)app {
_app = app;
self.iconView.image = [UIImage imageNamed:app.icon];
self.nameLabel.text = app.name;
self.descLabel.text = [NSString stringWithFormat:@"大小:%@ | 下載量:%@",app.size,app.download];
//覆蓋按鈕狀態
self.downloadBtn.enabled = (self.app.isDownloaded == NO);
}
這樣,各個cell裏按鈕的狀態就互不影響了