iOS实现一个包含若干页面和子页面的“打卡”App
开发环境
- Mac OS
- Objective-C
- Xcode
实验目的
- 学习使用纯代码进行UI布局
- 学习TableView,UICollectionView,UINavigationController,UICollectionController,UITabBarController等组件的使用,以及delegate和protocol的概念。
- 学习使用UIView动画及Core Animation动画
项目实现
一、创建一个Xcode项目
点击File->New->Project,选择ios下的Single View App,创建一个名为clockin的项目。
二、项目结构
AppDelegate
由于该打卡App包含登录、发现、打卡等多个页面,所以需要多个类来标识不同的页面。通过一个底部导航栏来控制发现、打卡、我的三个页面之间的跳转。所以需要在AppDelegate文件中初始化一个tabBar控制器并设置该控制器为window的根控制器,再通过不同类创建不同的子控制器,分别是发现页、打卡页和“我的”页。将其添加进根控制器中,即可实现点击底部导航栏跳转到不同的页面。
此外,当每个tabbaritem处于选中/未选中状态时,图标的颜色应该是不同的,可以通过设置tabBarItem.selectedImage和tabBarItem.image,来使得选中时选择有颜色的图标,未选中时选择灰暗的图标。
相关代码
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
//1.创建Window
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
//a.初始化一个tabBar控制器
UITabBarController *tb=[[UITabBarController alloc]init];
//设置控制器为Window的根控制器
self.window.rootViewController=tb;
//b.创建子控制器
DiscoverViewController *c1=[[DiscoverViewController alloc]init];
UINavigationController *c1Controller;
c1Controller = [[UINavigationController alloc] initWithRootViewController:c1];
c1Controller.tabBarItem.title = @"发现";
c1Controller.tabBarItem.selectedImage = [UIImage imageNamed:@"33/discovery.png"];
c1Controller.tabBarItem.image = [UIImage imageNamed:@"33/discovery2.png"];
AddClockInViewController *c2=[[AddClockInViewController alloc]init];
UINavigationController *c2Controller;
c2Controller = [[UINavigationController alloc] initWithRootViewController:c2];
c2Controller.tabBarItem.title = @"打卡";
c2Controller.tabBarItem.selectedImage = [UIImage imageNamed:@"33/clock.png"];
c2Controller.tabBarItem.image = [UIImage imageNamed:@"33/clock2.png"];
SigninController *c3=[[SigninController alloc]init];
UINavigationController *c3Controller;
c3Controller = [[UINavigationController alloc] initWithRootViewController:c3];
c3Controller.tabBarItem.title = @"我的";
c3Controller.tabBarItem.selectedImage = [UIImage imageNamed:@"33/mine.png"];
c3Controller.tabBarItem.image = [UIImage imageNamed:@"33/mine2.png"];
tb.viewControllers=@[c1Controller,c2Controller,c3Controller];
//2.设置Window为主窗口并显示出来
[self.window makeKeyAndVisible];
return YES;
}
SigninController 登录页
登录页要求很简单,只需有个圆形的登录按钮和一个背景渐变效果。点击登录按钮时需要跳转到个人信息页面。按钮事件相关代码如下:
[btn addTarget:self action:@selector(BtnClick:) forControlEvents:UIControlEventTouchUpInside];
-(void)BtnClick:(UIButton *)btn{
PersonalMessageViewController *nextVC = [[PersonalMessageViewController alloc]init];
NSMutableArray *v = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
[v removeObjectAtIndex:0];
[v addObject:nextVC];
[self.navigationController setViewControllers:v animated:YES];//从当前界面到nextVC这个界面
}
设置圆形按钮的关键在于将按钮的layer的cornerRadius大小设置为Button宽高的一半。即若按钮的长和宽为200,则layer的cornerRadius应该设置为100。
PersonalMessageViewController 个人信息
该页面是登录后跳转的页面,显示用户的个人资料,包括头像、用户名、邮箱、电话。还有一些关于app的信息:版本号、隐私、cookie等。该页面是一个静态的页面,主要是调整view的大小和布局,将不同的文字信息放在不同的label中,再将属于同一个view的label集合在一块。该页的设计比较简单,只需注意一下字体大小、边框大小和颜色、各元素之间的位置即可。
完成效果大致如下:
Single 单实例模板类
该类中存储着一个NSMutableArray数据结构,名为datalist,其作用是存储发现页中时间、地点、景点、心得和图片。通过在不同的文件中调用该类的datalist,可以实现数据的传输。
Single.h
#import <UIKit/UIKit.h>
@interface Single:NSObject
@property(nonatomic, strong)NSMutableArray *datalist;
+(instancetype)sharedInstance;
@end
Single.m
#import "Single.h"
#import <Foundation/Foundation.h>
@implementation Single
-(id)init{
if(self = [super init]){
self->_datalist = [[NSMutableArray alloc]init];
}
return self;
}
+(instancetype)sharedInstance{
static Single *myInstance = nil;
if(myInstance == nil){
myInstance = [[Single alloc]init];
}
return myInstance;
}
@end
DiscoverViewController 添加打卡页
- 输入框
根据要求,用户可以在该页输入时间、地点、景点名称、旅游心得,并且还可以上传图片。由于前三个都是单行输入框,因此考虑用UITextField实现,旅游心得是多行输入框,用UITextView实现。对于UITextField,提示信息的设置只需设置其placeholder属性即可,而对于UITextView,没有设置提示信息的方法,因此必须手动设置。想法是在UITextView中添加一个label,当检测到UITextView中的text长度为0时,显示label中的提示信息,反之,将其隐藏。相应代码如下:
- (void) textViewDidChange:(UITextView *)textView{
if ([textView.text length] == 0) {
[_label setHidden:NO];
}else{
[_label setHidden:YES];
}
}
- 图片的上传
设置一个按钮,为按钮设置一个点击事件。点击时判断当前已加入的图片是否已达到九张,若达到则提示用户最多只能上传九张图片,反之,调用系统相册从中选取一张图片,将选取的图片在添加打卡页面上显示出来并将其添加到一个NSMutableArray数据结构中。 - 发布按钮
设置一个发布按钮,点击后,获取所有输入框中的数据和NSMutableArray中所存储的图片,将其添加到单实例类的datalist中,以便发现页可以显示数据。按钮事件如下:
-(void)BtnClick2:(UIButton *)btn{
//datalist
NSDictionary *temp = [[NSDictionary alloc]init];
temp = @{@"data":_textField1.text,
@"place":_textField2.text,
@"spot":_textField3.text,
@"acquaintance":_textview.text,
@"image":_imagelist.mutableCopy
};
[[Single sharedInstance].datalist addObject:temp];
[_imagelist removeAllObjects];
self.count = 0;
[self showDismissWithTitle:@"" message:@"发布成功" parent:self];
self.tabBarController.selectedIndex = 0;//跳转到发现页
[self loadView];
}
QueryInformationViewController 查询页
点击发现页中的打卡记录跳转到该页,显示不同的打卡信息。由于打卡信息有时间、地点、景点、心得和图片。所以,与添加打卡页类似,需要若干个label和一个textview来显示信息,还需要UIImageView来显示图片。声明所需要的变量如下:
@property(strong,nonatomic) UILabel *label1;
@property(strong,nonatomic) UILabel *label2;
@property(strong,nonatomic) UILabel *label3;
@property(strong,nonatomic) UITextView *textview;
@property(strong,nonatomic) UIView *viewww;
@property(strong,nonatomic) UIImageView *v;
DiscoverViewController 发现页
- 背景渐变的实现
-(void)setBack{
CAGradientLayer *l = [[CAGradientLayer alloc] init];
l.colors = @[(__bridge id)UIColorFromRGB(0x87cefa).CGColor , (__bridge id)UIColorFromRGB(0xffc0cb).CGColor];
l.startPoint = CGPointMake(0, 0);
l.endPoint = CGPointMake(1, 1);
l.frame = self.view.bounds;
[self.view.layer addSublayer:l];
}
- 搜索框的实现
先声明一些属性:
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UISearchController *searchController;
// 搜索结果数组
@property (nonatomic, strong) NSMutableArray *results;
再初始化相关属性:
- (UITableView *)tableView {
if (_tableView == nil) {
_tableView = [[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
[self.view addSubview:_tableView];
}
return _tableView;
}
- (NSMutableArray *)results {
if (_results == nil) {
_results = [NSMutableArray arrayWithCapacity:0];
}
return _results;
}
创建UISearchController, 使用当前控制器来展示结果
UISearchController *search = [[UISearchController alloc]initWithSearchResultsController:nil];
search.searchResultsUpdater = self;
search.obscuresBackgroundDuringPresentation = NO;
self.searchController = search;
self.tableView.tableHeaderView = search.searchBar;
再实现tableView的数据源及代理方法,由于tableview代理方法较长,代码就不贴了。并最后实现 UISearchController 的协议 UISearchResultsUpdating方法:
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *inputStr = searchController.searchBar.text ;
if (self.results.count > 0) {
[self.results removeAllObjects];
}
for (NSDictionary *dictionary in [Single sharedInstance].datalist) {
//NSString* str = [dictionary[@"date"] componentsJoinedByString:@""];
NSString* str = dictionary[@"data"];
NSString* str2 = dictionary[@"place"];
NSString* str3 = dictionary[@"acquaintance"];
str = [str stringByAppendingString:str2];
str = [str stringByAppendingString:str3];
if ([str.lowercaseString rangeOfString:inputStr.lowercaseString].location != NSNotFound) {
[self.results addObject:dictionary];
}
}
[self.tableView reloadData];
}
通过实现 UISearchController 的协议 UISearchResultsUpdating方法,可以实现对输入时间、地点对打卡信息进行快速检索。
- 打卡记录排序
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"data" ascending:NO];
[[Single sharedInstance].datalist sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
- 点击打卡记录跳转查看详细信息
用QueryInformationViewController页面来展示信息,为了使得点击不同的打卡记录能显示不同的信息,需要找到该打卡记录所对应的datalist中所存储的信息。通过实现tabelview的代理方法:didSelectRowAtIndexPath,使得点击时获取点击位置并将datalist中所存储对应的数据传递给查询页,使得数据在查询页中显示。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
CGRect screenFrame = [UIScreen mainScreen].bounds;
int screenWidth = screenFrame.size.width;
int screenHeight = screenFrame.size.height;
QueryInformationViewController *newpage = [[QueryInformationViewController alloc] init];
newpage.label1 = [[UILabel alloc]initWithFrame:CGRectMake(30, screenHeight/10, 100, 60)];
newpage.label1.text = [Single sharedInstance].datalist[indexPath.section][@"data"];
newpage.label2 = [[UILabel alloc]initWithFrame:CGRectMake(30, screenHeight/10+30, 100, 60)];
newpage.label2.text = [Single sharedInstance].datalist[indexPath.section][@"place"];
newpage.label3 = [[UILabel alloc]initWithFrame:CGRectMake(30, screenHeight/10+60, 100, 60)];
newpage.label3.text = [Single sharedInstance].datalist[indexPath.section][@"spot"];
newpage.textview = [[UITextView alloc]initWithFrame:CGRectMake(27, screenHeight/10+100, screenWidth/6 * 5, screenHeight/3)];
[newpage.textview setText:[Single sharedInstance].datalist[indexPath.section][@"acquaintance"]];
newpage.viewww = [[UIView alloc]init];
NSMutableArray *ma = [Single sharedInstance].datalist[indexPath.section][@"image"];
for(UIImage *image in ma){
UIImageView *t = [[UIImageView alloc] initWithFrame:CGRectMake(40+75*(_count%3)-10, screenHeight/3 * 2+75*(_count/3)-50, 70, 70)];
_count++;
t.image = image;
[newpage.viewww addSubview:t];
}
_count = 0;
[self customPresentWith:@"cube"controller:newpage];//转场动画
}
- 转场动画的实现
- (void)customPresentWith:(NSString *)type
controller:(QueryInformationViewController *)view{
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:view];
CATransition *animation = [CATransition animation];
animation.duration = 1.0;
animation.timingFunction = UIViewAnimationCurveEaseInOut;
animation.type = type;
animation.subtype = kCATransitionFromLeft;
[self.view.window.layer addAnimation:animation forKey:nil];
[self presentViewController:nav animated:YES completion:nil];
}
- 值得注意
由于发现页的数据会随着打卡数据的增加而增加,因此,该页面应该是动态刷新的,当添加打卡后返回发现页时,发现页应该要被刷新。因此,需要将动态更新的部分代码放进viewWillAppear中,而不是viewDidLoad。
按时间排序:
搜索: