iOS - 30个Swift项目

概述

自学 iOS 和 Swift 也有一段时间了,最早尝试写Demo时都是向着相对较完整的App方向进行的,至此这样“相对完整的App”也只完成了三个,但是到头来学到的最大干货只有 CoreData 和 JSON解析,大部分时间用在不断重复的布局设置等方面,甚至有时为了画一些图标都要对着PS扣一晚上。所以对自己的学习安排做了一些改变,半个月前开始尽量保证每天写一个简单的Demo,当然这也是受到一些大佬们的启发,在网上有不少类似于《自学 iOS - 三十天三十个 Swift 项目 》这样的文章。
这次练习计划的目的完全是为了掌握一些新的知识和技巧,省去了”为了让Demo看上去更完整“而浪费的时间。
所有的Demo我都已经开源——GitHub链接 ,我没有把这些练习放在一个工程里面所以在下面的每一个Demo的记录中都会单独附上链接。
因为是自学所以代码规范方面一直有一些问题,但是做练习的同时也需要不断参考其他大佬们的代码,所以整个学习计划其实也是见证自己的代码逐渐规范化的一个过程。Demo我会坚持写,写这篇文章的目的一方面是希望给有缘看到这篇文章的初学者们一些帮助,另一方面坚持更新也是对自己学习过程的监督。

Demo

1.自定义导航栏透明渐变

1-1.gif

使用的是自定义导航栏,设置系统导航栏为透明的方法网上有很多但是在iOS 11中透明的导航栏默认为白色还不知道怎么解决,所以先用自定义的代替了。透明渐变的功能在scrollViewDidScroll 这个方法中实现,根据滚动偏移量计算百分比数值并将其作为导航栏透明度。

2.MapKit简单使用

2-1.gif

MapKit的简单使用,import前需要进行下面的设置:


2-2.jpg

添加地点时我是使用了CoreData,如果只是单纯的练习MapKit是没必要这样做的所以也算是对之前的知识做了复习。左划删除则是iOS 11里的新方法,代替以前的editActionsForRowAtIndexPath 方法:

    override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let actionDel = UIContextualAction(style: .destructive, title: "删除") { (action, view, finished) in
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            let context = appDelegate.persistentContainer.viewContext
            context.delete(self.fc.object(at: indexPath))
            appDelegate.saveContext()
            finished(true)
        }
        actionDel.backgroundColor = UIColor.red
        return UISwipeActionsConfiguration(actions: [actionDel])
    }

3.图片无限轮播

3.gif

使用scrollView实现图片无限轮播的功能,在scrollView中添加左、中、右三个imageView。使用下面的方法设置三个imageView的image:

    func setImg() {
        if index == 0 {
            leftImg?.image = UIImage(named: imgs.last!)
            midImg?.image = UIImage(named: imgs.first!)
            rightImg?.image = UIImage(named: imgs[1])
        } else if index == imgs.count - 1 {
            leftImg?.image = UIImage(named: imgs[index - 1])
            midImg?.image = UIImage(named: imgs.last!)
            rightImg?.image = UIImage(named: imgs.first!)
        } else {
            leftImg?.image = UIImage(named: imgs[index - 1])
            midImg?.image = UIImage(named: imgs[index])
            rightImg?.image = UIImage(named: imgs[index + 1])
        }
    }

自动滚动则是通过设置定时器实现。

4.两个tableView联动

4-1.gif

这次的练习是参考了一位大佬的文章【Swift联动】:两个TableView之间的联动,TableView与CollectionView之间的联动
在众多电商类App中都有用到联动tableView的形式。

5.新闻点击展开预览

5-1.gif

tableView自动行高的使用,设置行高为 UITableViewAutomaticDimension ,默认设置新闻内容label的行高为2,点击后变为0(即全部显示)
需要注意的是使用storyBoard进行约束时,必须将 tableViewCell 的 ContentView 从上到下填满,否则无法计算出当前行高。

6.AVKit简单使用

6-1.gif

系统自带播放器的简单使用,点击cell中的播放按钮调用下面的方法:

    @objc func playAction(_ sender: UIButton) {
        let path = Bundle.main.path(forResource: "清醒梦", ofType: "mp3")
        videoPlayer = AVPlayer(url: NSURL(fileURLWithPath: path!) as URL)
        playVideoController.player = videoPlayer
        self.present(playVideoController, animated: true) {
            self.playVideoController.player?.play()
        }
    }

这里因为我电脑里没有现成的视频文件所以偷懒用音频文件代替了...

7.自定义转场动画

7-1.gif

这个练习感觉是至今为止写过最实用的一个🤔,这次练习是分成两次做的。

  • 自定义转场动画,创建跳转和返回的两个动画类并分别实现UIViewControllerAnimatedTransitioning协议,具体的动画写在animateTransition()方法里,在fromView(也就是present方法的view)中实现UIViewControllerTransitioningDelegate协议。这里我是实现了类似于添加导航栏后系统自带的push动画。
  • 手势交互,同样是模仿系统push动画的拖动返回手势,在toView中添加pan手势,创建UIPercentDrivenInteractiveTransition属性并实现手势百分比更新,感谢大佬的文章关于自定义转场动画,我都告诉你。
    var secondViewController = SecondViewController()
    @objc func panAction(panGesture: UIPanGestureRecognizer) {
        let panPercent = panGesture.translation(in: self.view).x / self.view.frame.width
        switch panGesture.state {
        case .began:
            self.percentDrivenTransition = UIPercentDrivenInteractiveTransition()
            self.dismiss(animated: true, completion: nil)
        case .changed:
            self.percentDrivenTransition?.update(panPercent)
        case .ended, .cancelled:
            if panPercent > 0.5 {
                self.percentDrivenTransition?.finish()
            } else {
                self.percentDrivenTransition?.cancel()
            }
            self.percentDrivenTransition = nil
        default:
            break
        }
    }

8.tabBar简单使用

8-1.gif

tabBar的简单使用,主要是storyBoard上面的用法,前两个view都是之前的练习复制过来的,第三个view则是简单copy了一下微信的个人信息界面......
(大红色的导航栏可以说是很蠢了)

9.WKWebView简单使用

9-1.gif

使用WKWebView进行网页访问:

        let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
        self.view.addSubview(webView)
        if let exUrl = URL(string: "https://www.baidu.com") {
            let request = URLRequest(url: exUrl)
            webView.load(request)
        }

注意,苹果现在要求App内访问的网络必须使用HTTPS协议,但是很多网页是使用HTTP协议的,所以需要在 Info.plist 文件内做添加图中的内容:


9-2.png

同时复习了使用storyboard中的segue进行传值。
(tableView和网页访问的内容,可以无视...)

10.拖动手势&图片放大

10-1.gif

这个练习是在 自定义动画第二部分-交互手势 之前进行的,重点有两个:

  • 练习pan拖拽手势的使用,根据拖拽偏移量计算一些相关数值以实现实时的动画效果,同时也复习了动画的简单使用
  • 将imageView嵌套在ScrollView中,以此实现图片放大的效果

有两个小知识点需要注意:

  • 实现父视图半透明而子试图不透明的效果,可以通过设置
    view.backgroundColor = UIColor.black.withAlphaComponent(0.9)
    来实现
  • 如果要对UIImageView添加点击、拖拽等手势,需要先设置
    imageView.isUserInteractionEnabled = true

11.仿聊天界面

11-1.gif

这个练习耗费的时间比较多。

  • 最开始的时候我对发送和接受气泡的处理是写在同一个cell里,判断是发送消息还是接受消息后将另外一组气泡图片和label隐藏,但是仍然需要写两个cell(时间cell 和 消息cell),所以我没有使用storyboard而是想纯代码布局。因为涉及到了自动行高需要做约束,所以使用了SnapKit,但是由于个人能力有限不知道哪里出了错,失败了...不能设置自动行高。所以简单了解了xib的使用后用xib设置tableViewCell,同时对发送接收的处理换了方法——发送和接收设置两个tableViewCell,判断后返回相应的cell,达到了目的效果。
  • 信息发送栏随键盘的弹出和隐藏而改变高度,在viewDidLoad中设置键盘弹出和隐藏的通知并在方法中获取到键盘高度,使用动画改变发送栏的高度。

12.仿QQ个人资料页下拉背景图放大效果

12-1.gif

实现了类似于QQ个人资料页下拉背景图放大的效果,依然是在scrollViewDidScroll这个方法里做文章,当y方向偏移量<= 0 时,让图片的高度等于偏移量也就是将图片的高度固定不动,计算偏移量和屏幕宽度的比值,增加imageView的宽和高。同时为了整体效果我把第一个练习里的透明渐变导航栏也加进来了。

13.tableView中嵌入collectionView

13-1.gif

实现tableViewCell中嵌入collectionView,点击collectionView中的图片,将值传给tableView后单独刷新第一行的数据实现改变图片效果。同样是在电商类App中较常见。
单独刷新指定的
某个cell或某个section:

let index = IndexPath.init(row: 0, section: 0)
tableView.reloadRows(at: [index], with: .fade)

14.仿登录界面但实质是关键帧动画练习

14-1.gif
14-2.gif
14-3.gif
  • 昨天本来是想做个模仿Twitter启动动画的练习,看了一些成品的Demo后发现,关键帧动画真的,无敌好玩!!!所以花了点时间看了看有关关键帧动画的东西。简单写了个动画但是时间太晚了没有上传。
    所以今天连同昨天的练习一起,写了一个动画效果勉强还算好看(大概吧)的模拟登录界面
  • duration 设置动画时间,在 values 中设置关键帧,keytimes 中设置对应的时间百分比。timingFunctions中设置对应的速度控制。因为设置了layer.cornerRadius切割成圆角,所以为了保持按钮等组件的圆角不变在使用关键帧动画的同时也混用了基础动画。
  • 在ListViewController中,模仿大佬的demo做了一个tableViewCell背景颜色梯度变化的效果,并且设置visibleCells将cell藏在屏幕底部,使用Spring动画实现弹性效果。

15.本地推送

15.gif

使用UserNotifications实现本地推送通知,代码均写在AppDelegate中。参考自大神故胤道长Swift30Projects

16.实现用户更改头像操作

16-1.gif

调用系统的相机和相册,模拟应用中用户更改头像的操作。使用 UIImagePickerControllerDelegate 和 UINavigationControllerDelegate 这两个代理,并调用完成图片选择的代理方法实现头像更改:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        imgView.image = info[UIImagePickerControllerOriginalImage] as? UIImage
        dismiss(animated: true, completion: nil)
    }
  • 注意:调用相册和相机需要获得访问权限,在 Info.plist 文件中设置:
    16-2.jpg

17.tableView设置索引

17-1.gif

给tableView添加索引功能,使用其代理方法 sectionForSectionIndexTitle:

    func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
        var indexSection : Int = 0
        for i in sections {
            if i == title {
                return indexSection
            }
            indexSection += 1
        }
        return 0
    }

18.搜索栏使用

18-1.gif

搜索栏的使用,定义一个UISearchController,使用UISearchResultsUpdating并实现其代理方法:

    func updateSearchResults(for searchController: UISearchController) {
        if var text = searchController.searchBar.text {
            text = text.trimmingCharacters(in: .whitespaces)
            searchFilter(text: text)
            marvelTableView.reloadData()
        }
    }

使用数组的filter函数对原始数据进行筛选以实现搜索功能:

    func searchFilter(text: String) {
        self.searchResults = movies.filter({ (movie) -> Bool in
            return movie.name.localizedCaseInsensitiveContains(text)
        })
    }

19.仿微博界面 实现字数限制功能

19-1.gif
  • 模仿发微博界面,实现发微博时字数限制为140字的功能,超出后右下角字数统计label会变为橘色。
  • 使用通知来实现字数限制功能。在viewDidLoad中添加通知:
NotificationCenter.default.addObserver(self, selector: #selector(textViewNotificationAction(notification:)), name: NSNotification.Name.UITextViewTextDidChange, object: nil)

该通知调用方法 textViewNotificationAction :

    @objc func textViewNotificationAction(notification: Notification) {
        let limit: Int = 140
        let text = self.textView.text as NSString
        if text.length >= limit {
            let str = text.substring(to: limit)
            self.textView.text = str
            self.limitLabel.text = "\(limit)"
            self.limitLabel.textColor = UIColor.orange
        } else {
            self.limitLabel.textColor = UIColor.darkGray
            self.limitLabel.text = "\(text.length)"
        }
        self.weiboDetail = String(text)
    }

20.几个自己不常用的控件练习合集

20-1.gif

几个以前没用怎么用过的控件的练习合集,包括选择器UISegmentedControl、滑块UISlider、数字UIStepper、开关UISwitch。

21.tableView编辑、cell移动、左右侧滑

21-1.gif
  • 右划置顶:
    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let actionTop = UIContextualAction(style: .normal, title: "置顶") { (action, view, finished) in
            let first = IndexPath(row: 0, section: 0)
            tableView.moveRow(at: indexPath, to: first)
            finished(true)
        }
        actionTop.backgroundColor = UIColor.orange
        return UISwipeActionsConfiguration(actions: [actionTop])
    }
  • 左划删除:
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let actionDel = UIContextualAction(style: .destructive, title: "删除") { (action, view, finished) in
            self.ints.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)
            finished(true)
        }
        
        actionDel.backgroundColor = UIColor.red
        return UISwipeActionsConfiguration(actions: [actionDel])
    }
  • 编辑:
    需要先设置实现代理方法canEditRowAt,返回值为true,练习中因为需要移动cell所以也需要实现canMoveRowAt,并实现moveRowAt方法:
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let fromRow = (sourceIndexPath as NSIndexPath).row
        let toRow = (destinationIndexPath as NSIndexPath).row
        let int = ints[fromRow]
        
        ints.remove(at: fromRow)
        ints.insert(int, at: toRow)
    }

22.使用截图完成动画

22-1.gif

使用截屏实现一些动画效果:

let snapshotView = selectedCell?.snapshotView(afterScreenUpdates: false)!

比如在demo中,CollectionView中的每一个Cell都有一个ImageView,动画开始前使用上面的代码给点击的cell截图,将其frame设置与原cell一致,并将原cell隐藏。接下来的位移及大小动画则均是以该截图为操作对象。
这种方法在自定义转场动画时更实用。

23.UIPageViewController的使用

23-1.gif
  • 练习中 PageViewController 的子视图是动态的,重复调用 WebViewController 每一页只是改变了 WKWebView 加载的url。在 PageViewController 中需要实现UIPageViewControllerDataSource的两个方法,返回当前页VC的前一个和后一个VC:
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        var index = (viewController as! WebViewController).index
        index -= 1
        return setViewController(index: index)
    }
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        var index = (viewController as! WebViewController).index
        index += 1
        return setViewController(index: index)
    }

在 setViewController 方法中为 WebViewController 设置相应的url和索引并返回

    func setViewController(index: Int) -> WebViewController? {
        if case 0..<urls.count = index {
            let main = UIStoryboard.init(name: "Main", bundle: Bundle.main)
            if let webVC = main.instantiateViewController(withIdentifier: "webView") as? WebViewController {
                webVC.url = urls[index]
                webVC.index = index
                
                return webVC
            }
        }
        return nil
    }
  • 练习中使用的url均选自简书-7日热门。简书网址使用HTTP协议所以需要在 Info.plist 文件中设置 App Transport Security Settings - Allow Arbitrary Loads 为 YES

24.使用UIPickerView模拟老虎机

24-1.gif
  • 使用pickerView模拟“老虎机”,模仿概述中提及的大佬的文章中的第14项。
  • 使用 arc4random() 方法生成随机数,在代理方法viewForRow中返回一个UILabel,以随机数作为下标选择emojiArray中的Emoji表情并分别赋值给给每一行的label.text:
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        let pickerLabel = UILabel()
        pickerLabel.font = UIFont(name: "Apple Color Emoji", size: 70)
        pickerLabel.textAlignment = .center
        switch component {
        case 0:
            pickerLabel.text = emojiArray[Int(arrayOne[row])]
        case 1:
            pickerLabel.text = emojiArray[Int(arrayTwo[row])]
        case 2:
            pickerLabel.text = emojiArray[Int(arrayThree[row])]
        default:
            break
        }
        return pickerLabel
    }

25.简单的画板功能

25-1.gif
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        let context = UIGraphicsGetCurrentContext()
        context?.setLineCap(.round)
        context?.setLineJoin(.round)
        
        if allLines.count > 0 {
            for i in 0..<allLines.count {
                let linePoints = allLines[i]
                if linePoints.count > 0 {
                    context?.beginPath()
                    context?.move(to: linePoints[0])
                    for j in 0..<linePoints.count {
                        context?.addLine(to: linePoints[j])
                    }
                    context?.setLineWidth(self.lineWidth)
                    context?.setStrokeColor(strokeColors[i])
                    context?.strokePath()
                }
            }
        }
        if currentPoints.count > 0 {
            context?.beginPath()
            context?.setLineWidth(self.lineWidth)
            context?.setStrokeColor(self.strokeColor.cgColor)
            context?.move(to: currentPoints[0])
            for i in 0..<currentPoints.count {
                context?.addLine(to: currentPoints[i])
            }
            context?.strokePath()
        }
    }

26.下拉刷新UIRefreshControl基本使用

26-1.gif
  • 下拉刷新控件UIRefreshControl的基本使用。
  • 设置两个保存Emoji表情的数组:
    var emojiArray = ["","","","","","","","🦄","",""]
    let refreshEmojiArray = ["","","","",""]

其中第一个数组是tableView默认显示的数据,第二个则是提供给刷新方法使用:

    @objc func refreshAction() {
        let arcCount = UInt32(self.refreshEmojiArray.count)
        let arc = Int(arc4random() % arcCount)
        self.emojiArray.insert(refreshEmojiArray[arc], at: 0)
        self.tableView.reloadData()
        self.refreshControl.endRefreshing()
    }

使用arc4random()方法生成随机数作为数组2的下标,并将对应的字符串添加到数组1的最开始位置(方便观察刷新效果)。

  • 用虚拟机做拖拽的动作真的有点不自在无论是鼠标还是触控板的三只拖拽...所以演示图片里好多次没有拖好...

27.通过简单的单词翻译功能Demo练习JSON解析

27-1.gif
  • 使用第三方库:SwiftyJSON进行JSON解析,接口来自扇贝单词
  • 主要目的还是,练习JSON,所以一些会导致程序崩溃的情况都没有做处理 比如搜索栏的单词必须正确不能有空格(好吧我承认其实是因为想偷懒了。。。)
  • 使用CAGradientLayer做了一个渐变的背景色,颜色提取自iPhone一张内置壁纸:
    func gradientColors() -> CAGradientLayer {
        
        let topColor = UIColor(named: "topColor")!
        let bottomColor = UIColor(named: "bottomColor")!
        let gradientColors = [topColor.cgColor, bottomColor.cgColor]
        let gradientLocations: [NSNumber] = [0.0, 1.0]
        let gradientLayer = CAGradientLayer()

        gradientLayer.colors = gradientColors
        gradientLayer.locations = gradientLocations
        return gradientLayer
    }

28.计算最大公约数和最小公倍数

28-1.gif

老实说,这不算是个练习。至于为啥写了这么个东西,是因为前两天考研的室友,专业课考C语言,他做的题里有计算这俩东西的题,让我拿Swift写一下。然后昨天晚上等球赛的时候,把在playground上写给他演示的代码写成了这个工程。。。

29.tableView分组折叠/展开效果

29-1.gif
  • 实现tableView分组折叠/展开的效果。
  • 参考项目的链接我找不到了。。。
  • 创建TableViewHeaderViewDelegate协议,给headerView添加tap点击手势并实现协议方法 tableViewHeaderClick
    func tableViewHeaderClick(_ headerView: TableViewHeaderView, section: Int) {

        let show = itemData[section].isShow
        itemData[section].isShow = !show!
        
        let index = IndexSet(integer: section)
        self.tableView.reloadSections(index, with: .fade)
    }
  • 其实demo中只有一个headerView写协议是有点多余了的但是毕竟Swift核心是面向协议,平时多用协议对以后的学习也有好处

30.Swift 自定义UICollectionViewLayout

30-1.gif

写在最后

由于偶尔偷懒,最后用了40天左右的时间写了这30个小练习,虽然难度都不大但是我通过这30个练习确实学到了不少东西。下一步打算开通开发者账号,试着以上架App Store为目的写一个完整的App。

推荐阅读更多精彩内容

  • 金灿灿的玉米囤满农家的庭院 红通通的小枣晾晒在屋前院后 黑黝黝的豆儿装好袋子聚在屋檐下 丰收!今年的秋,农家院落显...
    丰盈仓廪阅读 562评论 0 0
  • 寒风凛冽,寒气袭人,枯草萧疏,绿色尽失,今年的冬天如此苍白凄凉且冷漠. M点燃一支烟,吞云吐雾起来,一圈圈的烟雾,...
    幽谷泉涌阅读 568评论 0 1
  • 01 大熊和小琪是大学同学,也是我认识的人里,唯一一对毕业季在一起,结果谈了三年异地恋的恋人。 上周末,大熊还是失...
    子小洛阅读 3,287评论 47 61