iOS swift 3天30个Swift项目之第一天

参考项目:https://github.com/soapyigu/Swift-30-Projects

1 计时器

首先看下效果吧。

1.1 主要功能

点击播放按钮,数字开始增加;
点击暂停按钮,数字停止增加;
点击“Reset”,数值置为0;

1.2 UI

这里UI采用storyboard来实现。因为比较简单,就4个控件,拖进去故事版即可。

1.3 功能实现

ui声明,故事版可以直接拖进相关类中,自动生成:

@IBOutlet weak var playBtn: UIButton!
@IBOutlet weak var pauseBtn: UIButton!
@IBOutlet weak var timeLabel: UILabel!

显示的数字声明:

 // 浮点数默认是Double类型,若要使用Float,需要显示声明
    // var counter: Float = 0.0
    var counter: Float = 0.0 {
        // 属性观察器 
        didSet {
            timeLabel.text = String(format: "%.1f", counter)
        }
    }

这里的属性观察器是监听这个值变更情况,如果某个地方变更了这个值,那么会走里面的didSet回调。

    // 知识点:存储属性和计算属性
    override var preferredStatusBarStyle: UIStatusBarStyle {
        // 只读计算属性,可以去掉get和花括号
//        get {
//            return UIStatusBarStyle.lightContent
//        }
        return UIStatusBarStyle.lightContent
    }

上面的代码是控制状态栏图标颜色为亮色(白色),默认是黑色。

  // 给予timer一个默认值,这样timer就不会为Optional,
    // 后续可以不用再解包
    // var timer = Timer()
    
    // 这样定义可以在不用timer时回收内存
    var timer: Timer? = Timer()
    var isPlaying = false

这里定义了定时器,和定时器Flag。

override func viewDidLoad() {
        super.viewDidLoad()
        // 符合LosslessStringConvertible协议的,
        // 都可以直接初始化一个String对象
        // timeLabel.text = String(counter)
        
        // 改成使用属性观察器监控和响应属性值的变化
        counter = 0.0
    }

这里在初始化时设置数值为0.0。

  @IBAction func resetButtonDidTouch(_ sender: UIButton) {
        if let timerTemp = timer {
            timerTemp.invalidate()
        }
        timer = nil
        isPlaying = false
        counter = 0
        playBtn.isEnabled = true
        pauseBtn.isEnabled = true
    }

上面是重置按钮点击事件。

这个可以在故事版中右键视图,在Touch up inside中拖动到代码里面自动生成方法,具体实现逻辑由我们自行添加。这里是将定时器置为nil,然后将播放和暂停状态都设置为可点击状态。

@IBAction func playButtonDidTouch(_ sender: UIButton) {
        playBtn.isEnabled = false
        pauseBtn.isEnabled = true
        // 调用实例的方法时建议用self.UpdateTimer,
        // 不建议使用ViewController.UpdateTimer
        // 因为若方法定义成了类方法,第二种方式编译器不会报错。
        timer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector: #selector(self.UpdateTimer), userInfo: nil, repeats: true)
        isPlaying = true
    }

这里是播放按钮的实现逻辑,这里面相当于new了一个Timer。间隔0.1s后刷新,然后会走一个selector里面的updateTimer方法更新数值。

   @objc func UpdateTimer() {
        counter = counter + 0.1
    }

这里变更数字,然后数值变化引起前面定义的didSet刷新,导致Lable数值更新。

 @IBAction func pauseButtonDidTouch(_ sender: UIButton) {
        playBtn.isEnabled = true
        pauseBtn.isEnabled = false
        if let timerTemp = timer {
            timerTemp.invalidate()
        }
        timer = nil
        isPlaying = false
        
    }

这里是暂停按钮实现逻辑,将按钮置灰,定时器invalidate,然后定时器置空。

2 自定义字体

2.1 效果

2.2 字体文件

首先放置在根目录下:

这里XCode会自动识别出字体文件:

2.3 实现细节

控制器可直接实现UITableViewDelegate, UITableViewDataSource,也可以扩展,最好是扩展,可读性好一点。

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    static let identifier = "FontCell"

然后定义下数据和字体名称:

   var data = ["MFTongXin 30 Days Swift", "MFJinHei 这些字体特别适合打「奋斗」和「理想」",
                "MFZhiHei 谢谢「造字工房」,本案例不涉及商业使用", "Zapfino 使用到造字工房劲黑体,致黑体,童心体",
                "Gaspar呵呵,再见🤗 See you next Project", "微博 @Allen朝辉",
                "测试测试测试测试测试测试", "123", "Alex", "@@@@@@"]
    
    var fontNames = ["MFTongXin_Noncommercial-Regular",
                     "MFJinHei_Noncommercial-Regular",
                     "MFZhiHei_Noncommercial-Regular",
                     "Zapfino",
                     "Gaspar Regular"]

然后定义下UI:

  @IBOutlet weak var changeFontLabel: UILabel!
    @IBOutlet weak var fontTableView: UITableView!

然后定义下选择字体索引:

 var fontRowIndex = 0

生命周期初始化:

   
    override func viewDidLoad() {
        super.viewDidLoad()
        // 在storyboard中直接设置了
        // fontTableView.dataSource = self
        // fontTableView.delegate = self
        
        // 使用手势加Label替换button
        changeFontLabel.layer.cornerRadius = 50
        changeFontLabel.layer.masksToBounds = true
        // 设置为true才能响应手势
        changeFontLabel.isUserInteractionEnabled = true
        let gesture = UITapGestureRecognizer(target: self,
                                             action: #selector(changeFontDidTouch(_:)))
        changeFontLabel.addGestureRecognizer(gesture)
    }

     @objc func changeFontDidTouch(_ sender: AnyObject) {
        
        fontRowIndex = (fontRowIndex + 1) % 5
        print(fontNames[fontRowIndex])
        fontTableView.reloadData()
        
    }

这里主要是给一个按钮添加了手势,手势设置了action,然后相当于给按钮添加点击事件,方法有很多,可以直接故事版添加事件,也可以这种手势添加点击事件。

下面是TableView的协议实现,这里Cell用了默认的,无需新建Cell。

 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 35
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//        let cell = fontTableView.dequeueReusableCell(withIdentifier: ViewController.identifier)
        let cell = fontTableView.dequeueReusableCell(withIdentifier: ViewController.identifier, for: indexPath)
        let text = data[indexPath.row]
       
        cell.textLabel?.text = text
        cell.textLabel?.textColor = indexPath.row != fontRowIndex ? UIColor.white : UIColor.blue
        cell.textLabel?.font = UIFont(name: self.fontNames[fontRowIndex], size:16)
        
        return cell
    
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }

主要是在cellForRowAt显示了具体item效果,根据前面选择的字体索引,这里设置给cell了。

3 播放本地视频

3.1 效果

3.2 本地视频

直接将mp4文件放置在根目录下。
Xcode可自动识别到Movie文件夹下。

3.3 Core Data

这里播放本地视频并不依赖这个。
因为demo中有部分代码是这个,当然也是可以学习下的。
关于Core Data实际上是一个数据存储框架,类似Realm。
Ficow写的这篇文章还不错:
了解和采用 CoreData 框架 —— Ficow 陪你学 CoreData

3.4 UI定义

 // Swift中mark的使用方式,效果等同OC重的 #pragma mark -
   //MARK:- Variables
   @IBOutlet weak var videoTableView: UITableView!

这里就一个UITableView,展示一个列表。

  var playViewController = AVPlayerViewController()
  var playerView = AVPlayer()

然后这里定义并且初始化了视频播放关键类。
AVPlayerViewController是一个系统的控制器,用来播放视频的,这里播放本地视频其实也是跳转到系统默认的播放器来播放。

var data = [
        // 给项目编译后属于同一个module,所以Video不需要import就可以使用
        Video(image: "videoScreenshot01",
              title: "Introduce 3DS Mario",
              source: "Youtube - 06:32"),
        Video(image: "videoScreenshot02",
              title: "Emoji Among Us",
              source: "Vimeo - 3:34"),
        Video(image: "videoScreenshot03",
              title: "Seals Documentary",
              source: "Vine - 00:06"),
        Video(image: "videoScreenshot04",
              title: "Adventure Time",
              source: "Youtube - 02:39"),
        Video(image: "videoScreenshot05",
              title: "Facebook HQ",
              source: "Facebook - 10:20"),
        Video(image: "videoScreenshot06",
              title: "Lijiang Lugu Lake",
              source: "Allen - 20:30")
    ]

上面是列表页数据定义。

 //MARK:- View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        videoTableView.dataSource = self
        videoTableView.delegate = self
        
    }

初始化里面设置了数据源和代理。


//MARK:- UIViewTableView DataSource & Delegate
// 知识点:扩展
// 扩展和 Objective-C 中的分类类似,但没有名称
// 扩展可以为一个类型添加新的功能,但是不能重写已有的功能。
// 扩展可以添加新的计算型属性,但是不可以添加存储型属性,也不可以为已有属性添加属性观察器。
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    // Extensions must not contain stored properties
    //var a = 1
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 220
    }

    func numberOfSections(in tableView: UITableView) -> Int {
    return 2
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return data.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    // 知识点:向下转型
    // as! 强制类型转换,无法转换时会抛出运行时异常
    // as?可选类型转换,无法转换时返回nil
    let cell = videoTableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath) as! VideoCell
    let video = data[indexPath.row]

    cell.videoScreenshot.image = UIImage(named: video.image)
    cell.videoTitleLabel.text = video.title
    cell.videoSourceLabel.text = video.source

    return cell

    }
}

这里扩展实现了UITableView的代理和数据源。

这里用到了一个Cell需要自定义。


// 定义Video的结构体,属性初始化后不能被改变,因为结构体时值类型。
// 在你每次定义一个新类或者结构体的时候,实际上你是定义了一个新的 Swift 类型。
// 因此请使用UpperCamelCase这种方式来命名
struct Video {
    let image: String
    let title: String
    let source: String
}

class VideoCell: UITableViewCell {

    @IBOutlet weak var videoScreenshot: UIImageView!
    @IBOutlet weak var videoTitleLabel: UILabel!
    @IBOutlet weak var videoSourceLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

这个VideoCell是怎么添加视图的呢,当然也是故事版的作用了。
全局搜索了下VideoCell,发现Main.storyboard里面用到了这个VideoCell,这里关联到了故事版。

这里注意到右上角是一个Custom Class,说明故事版是可以关联自定义类的,这里就把Cell关联进来了。

然后还是在Main.storyboard中,给cell里面的botton添加了一个点击事件给控制器:

@IBAction func playVideoButtonDidTouch(_ sender: AnyObject) {
        let path = Bundle.main.path(forResource: "emoji zone", ofType: "mp4")
        
        playerView = AVPlayer(url: URL(fileURLWithPath: path!))
        
        playViewController.player = playerView
        
        // 知识点:尾随闭包
        // 在使用尾随闭包时,你不用写出它的参数标签
        // 如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉
        // 完整形式如下:
        //self.present(playViewController, animated: true, completion: {
        //    self.playViewController.player?.play()
        //})
        self.present(playViewController, animated: true) {
            self.playViewController.player?.play()
        }
    }
 }

这里给系统的控制器设置了一个AVPlayer,AVPlayer里面设置了URL,这样实现了播放视频效果。

4 摄像头

4.1 效果预览

4.2 UI

这里主要是一个横滑的UIScrollView。
长度设置了屏幕的3倍,第一块放置一个UIImageView,中间放置摄像头,最后一块也放置了一个UIImageView。

这里左侧的视图,采用了xib的方式来创建。
我个人理解,xib的方式有点类似Android的xml,因为我本身是学Android,希望大家原谅我这样比喻。xib也是很直观,可以看到效果,而且也可以挂载到某个具体的UIView上。

左侧的xib是这样的:

右侧的xib是这样的:

中间的就是一个控制器来的,当然这个控制器也是可以关联xib的。
如何关联的?
这里有一个CameraView.xib,它的File’s Owner是CameraView,这样就关联起来了。所以再CameraView.swift里面操作的都是针对这个xib的逻辑处理。

重点看下如何显示摄像头的吧:
首先是UI关联:

@IBOutlet weak var cameraView: UIView!

这个应该是拖进来的。

然后就是几个关键的类:

 var captureSession : AVCaptureSession?
 var stillImageOutput : AVCaptureStillImageOutput?
 var previewLayer : AVCaptureVideoPreviewLayer?

然后是将要显示的时候逻辑处理:

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        captureSession = AVCaptureSession()
        // 已经不能再使用了
        // captureSession?.sessionPreset = AVCaptureSessionPreset1920x1080
        captureSession?.sessionPreset = AVCaptureSession.Preset.hd1920x1080
        let backCamera = AVCaptureDevice.devices(for: .video).first!
        // 已经不能再使用了
        // let backCamera = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
        var error : NSError?
        var input: AVCaptureDeviceInput!
        
        do {
            input = try AVCaptureDeviceInput(device: backCamera) }
        catch let error1 as NSError {
            error = error1
            input = nil
        }
        
        if (error == nil && captureSession?.canAddInput(input) != nil) {
            
                captureSession?.addInput(input)
                
                stillImageOutput = AVCaptureStillImageOutput()
                stillImageOutput?.outputSettings = [AVVideoCodecKey : AVVideoCodecJPEG]
                
            if let stillImageOutputTemp = stillImageOutput {
                if captureSession?.canAddOutput(stillImageOutputTemp) != nil {
                    captureSession?.addOutput(stillImageOutputTemp)
                    if let captureSessionTemp = captureSession {
                        previewLayer = AVCaptureVideoPreviewLayer(session: captureSessionTemp)
                        previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
                        // 已经废弃不用了
                        // previewLayer?.videoGravity = AVLayerVideoGravityResizeAspect
                        previewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
                        cameraView.layer.addSublayer(previewLayer!)
                        captureSession?.startRunning()
                    }
                }
            }
        }
        
    }

主要是初始化了一个AVCaptureSession,然后又创建了一个AVCaptureDeviceInput来接收输入流,这样利用session.addInput,将输入流给input,然后搞一个输出流AVCaptureStillImageOutput,来接收captureSession.addOutPut,这样最后我们再创建一个 预览类叫做AVCaptureVideoPreviewLayer,里面放session,这样就得到一个Layer了。

这个Layer我们就可以加到普通的UIView上了。

4.3 根控制器

先定义一个UIScollView:


class ViewController: UIViewController {
    

    @IBOutlet weak var scrollView: UIScrollView!

初始化添加子View,3块布局给它add进去:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 已经废弃了,使用prefersStatusBarHidden属性返回设置的值
    // UIApplication.shared.isStatusBarHidden = true
    
    let screenWidth = UIScreen.main.bounds.width
    let screenHeight = UIScreen.main.bounds.height
    let leftView: UIViewController = UINib(nibName: "LeftView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UIViewController
    let centerView: CameraView = CameraView(nibName: "CameraView", bundle: nil)
    let rightView: RightView = RightView(nibName: "RightView", bundle: nil)
    
    leftView.view.frame = CGRect(x: 0, y: 0, width: screenWidth-200, height: screenHeight)
    centerView.view.frame = CGRect(x: screenWidth, y: 0, width: screenWidth, height: screenHeight)
    rightView.view.frame = CGRect(x: 2*screenWidth, y: 0, width: screenWidth, height: screenHeight)

    self.scrollView.addSubview(leftView.view)
    self.scrollView.addSubview(rightView.view)
    self.scrollView.addSubview(centerView.view)
    self.scrollView.contentSize = CGSize(width: screenWidth * 3, height: screenHeight)
}

frame要对应好屏幕坐标。
scrollView的contentSize刚好对应3个屏幕大小。

这里可以直接创建一个UINib作为控制器,传xib的名称给它就可以的。

另外这里用到了相机,需要相机的权限声明哦:
在info.plist文件下配置:
Privacy - Camera Usage Description

然后Value自己随便填下就好了。

5 传送带效果

5.1 效果

5.2 故事版关联

首先Main.storyboard是在info.plist中注册的。 然后这个首页故事版配置的控制器的Class就是代码区域表示的控制器了。 层层关联环环相扣。

5.3 UI

这里其实就一个背景图片+一个UICollectionView。

class HomeViewController: UIViewController {

    
    @IBOutlet weak var backgroundImageView: UIImageView!
    @IBOutlet weak var collectionView: UICollectionView!

5.4 Data

// 访问权限分物种:private,fileprivate,internal,public 和 open
    // private:只能在本类的作用域且在当前文件内能访问
    // fileprivate:只能在当前文件内能访问
    // internal:本module内能访问
    // public:跨module访问但不能重写或继承
    // open:跨module访问并且能重写或继承
    fileprivate var interests = Interest.createInterests()
class Interest
{
    // MARK: - Public API
    var title = ""
    var description = ""
    var numberOfMembers = 0
    var numberOfPosts = 0
    var featuredImage: UIImage!
    
    init(title: String, description: String, featuredImage: UIImage!)
    {
        self.title = title
        self.description = description
        self.featuredImage = featuredImage
        numberOfMembers = 1
        numberOfPosts = 1
    }
    
    // MARK: - Private
    // dummy data
    static func createInterests() -> [Interest]
    {
        return [
            Interest(title: "Hello there, i miss u.", description: "We love backpack and adventures! We walked to Antartica yesterday, and camped with some cute pinguines, and talked about this wonderful app idea. 🐧⛺️✨", featuredImage: UIImage(named: "hello")!),
            Interest(title: "🐳🐳🐳🐳🐳", description: "We love romantic stories. We walked to Antartica yesterday, and camped with some cute pinguines, and talked about this wonderful app idea. 🐧⛺️✨", featuredImage: UIImage(named: "dudu")!),
            Interest(title: "Training like this, #bodyline", description: "Create beautiful apps. We walked to Antartica yesterday, and camped with some cute pinguines, and talked about this wonderful app idea. 🐧⛺️✨", featuredImage: UIImage(named: "bodyline")!),
            Interest(title: "I'm hungry, indeed.", description: "Cars and aircrafts and boats and sky. We walked to Antartica yesterday, and camped with some cute pinguines, and talked about this wonderful app idea. 🐧⛺️✨", featuredImage: UIImage(named: "wave")!),
            Interest(title: "Dark Varder, #emoji", description: "Meet life with full presence. We walked to Antartica yesterday, and camped with some cute pinguines, and talked about this wonderful app idea. 🐧⛺️✨", featuredImage: UIImage(named: "darkvarder")!),
            Interest(title: "I have no idea, bitch", description: "Get up to date with breaking-news. We walked to Antartica yesterday, and camped with some cute pinguines, and talked about this wonderful app idea. 🐧⛺️✨", featuredImage: UIImage(named: "hhhhh")!),
        ]
    }
}

这里创建了一个静态函数,就是数据定义了。

5.5 绑定数据

extension HomeViewController : UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return interests.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Storyboard.CellIdentifier, for: indexPath) as! InterestCollectionViewCell
        
        cell.interest = self.interests[indexPath.item]
        
        return cell
        
    }
}

这里就是UICollectionView的数据源了。
对应的Cell为InterestCollectionViewCell

这里注意到这个UICollectionView好像没有设置dataSource和delegate,这里看了下Main.storyboard里面,果然在右侧看到Outlets设置了dataSource和delegate都给了控制器。所以说在故事版里面是可以设置数据源和代理的。

如下:

class InterestCollectionViewCell: UICollectionViewCell {
    
    
    var interest: Interest! {
        didSet {
            updateUI()
        }
    }
    
    @IBOutlet weak var featuredImageView: UIImageView!
    @IBOutlet weak var interestTitleLabel: UILabel!
    
    fileprivate func updateUI() {
        interestTitleLabel?.text! = interest.title
        featuredImageView?.image! = interest.featuredImage
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        self.layer.cornerRadius = 5.0
        self.clipsToBounds = true
    }
}

因为在Main.storyboard的已经关联了这个Cell,所以布局相关的都无需重新addView。

上面这些代码就可以实现一个传送带效果。

但是我们想要左滑,它自己能够刚好滑动到下一个屏幕,体验效果好一点的话要怎么办呢?

我们想到第一个优化点:
一张图能不能刚好占一个屏幕,增大一点padding。

可以的。

利用UICollectionView提供的另外一个协议可以实现。答案就是:UICollectionViewDelegateFlowLayout。

extension HomeViewController : UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: UIScreen.main.bounds.width - 2 * Storyboard.CellPadding, height: 450)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 2 * Storyboard.CellPadding
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsetsMake(0, Storyboard.CellPadding, 0, Storyboard.CellPadding)
    }
}

这里第一个sizeForItemAt方法:单个item的大小,长度为屏幕宽度-左右两个padding,高度写死450。
这里第二个方法minimumLine: collectionView的滚动方向为水平时,这个就是item之间的最小水平间距。
这里的第三个方法insetForSectionAt: 单个item的padding,设置上,左,右,下。

第二个优化点:当滑动到一半多,自动切下一张,不足一半,切回上一张。
也好办。

extension HomeViewController : UIScrollViewDelegate {
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let originPoint = targetContentOffset.pointee;
        var index = Int(originPoint.x / UIScreen.main.bounds.width)
        let offset = Int(originPoint.x) % Int(UIScreen.main.bounds.width)
        index += (offset > Int(UIScreen.main.bounds.width/2) ? 1 : 0)
        targetContentOffset.pointee = CGPoint(x: index * Int(UIScreen.main.bounds.width) , y: 0)
    }
}

继续扩展下这个控制器,这里面,判断了offset在停止滑动后的逻辑。

6 定位

6.1 效果

6.2 UI

这个UI比较简单,一个UILabel,一个按钮。

这个按钮的点击事件直接通过故事版链接到了控制器,所以控制器无需有这个UIView,有这个点击事件就行了。

6.3 如何定位

首先声明一个全局变量:

  // 强制自动解包,可以赋值为nil,为nil后再调用会报错
    // 建议定义为:
    // var locationManager: CLLocationManager
    var locationManager: CLLocationManager!

开始定位:

@IBAction func myLocationButtonDidTouch(_ sender: AnyObject) { 
    locationManager = CLLocationManager()
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestAlwaysAuthorization()
    locationManager.startUpdatingLocation()   
}

因为这里引入了CLLocationManager,那么在项目层级发现CoreLocation.framework自动引入进来了。

直接这样还不够,还需要设置下协议才行。因为这里我们delegate设置了self。
这里我们同样可以新建一个Delegate,本质上扩展控制器,当然也可以直接写在控制器里面,都可以,最好是分一个文件,可读性会强一点。

import Foundation
import CoreLocation

extension ViewController : CLLocationManagerDelegate {
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        
        self.locationLabel.text = "Error while updating location " + error.localizedDescription
        
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        CLGeocoder().reverseGeocodeLocation(locations.first!) { (placemarks, error) in
            guard error == nil else {
                self.locationLabel.text = "Reverse geocoder failed with error" + error!.localizedDescription
                return
            }
            if placemarks!.count > 0 {
                let pm = placemarks!.first
                self.displayLocationInfo(pm)
            } else {
                self.locationLabel.text = "Problem with the data received from geocoder"
            }
        }
    }
    
    func displayLocationInfo(_ placemark: CLPlacemark?) {
        if let containsPlacemark = placemark {
            //stop updating location to save battery life
            locationManager.stopUpdatingLocation()
            
            let locality = (containsPlacemark.locality != nil) ? containsPlacemark.locality : ""
            let postalCode = (containsPlacemark.postalCode != nil) ? containsPlacemark.postalCode : ""
            let administrativeArea = (containsPlacemark.administrativeArea != nil) ? containsPlacemark.administrativeArea : ""
            let country = (containsPlacemark.country != nil) ? containsPlacemark.country : ""
            
            self.locationLabel.text = postalCode! + " " + locality!
            
            self.locationLabel.text?.append("\n" + administrativeArea! + ", " + country!)
        }
        
    }
}

7 下拉刷新

7.1 效果

7.2 变量声明

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    var index = 0
    let cellIdentifer = "NewCellIdentifier"
    
    let favoriteEmoji = ["🤗🤗🤗🤗🤗", "😅😅😅😅😅", "😆😆😆😆😆"]
    let newFavoriteEmoji = ["🏃🏃🏃🏃🏃", "💩💩💩💩💩", "👸👸👸👸👸", "🤗🤗🤗🤗🤗", "😅😅😅😅😅", "😆😆😆😆😆" ]
    var emojiData = [String]()
    var tableView: UITableView!
    
    var refreshControl = UIRefreshControl()
    var navBar: UINavigationBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: 375, height: 64))

这里继承了UIViewController,然后再Main.storyboard中配置了首页的控制器就是这个,这样就直接启动这个控制器了。

这里定义了初始化数据3条,更新后的6条。
模拟刷新后的数据变化。

这里不是再故事版里面加视图,直接在代码里面new了。

7.3 viewDidLoad初始化

 override func viewDidLoad() {
        super.viewDidLoad()
        // 这里new一个UITableView
        tableView = UITableView(frame: self.view.bounds, style: .plain)
        
        // 初始数据
        emojiData = favoriteEmoji
        let emojiTableView = tableView
        
        // 数据源设置,注册Cell,这里的Cell比较简单,直接用了UITableViewCell可满足
        emojiTableView?.backgroundColor = UIColor(red:0.092, green:0.096, blue:0.116, alpha:1)
        emojiTableView?.dataSource = self
        emojiTableView?.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifer)
        
        // 因为TableView是UIScrollViw,这里也有这个refreshControl的属性
        tableView.refreshControl = self.refreshControl
        self.refreshControl.addTarget(self, action: #selector(ViewController.didRoadEmoji), for: .valueChanged)
        
        // 自定义刷新器 这里的loading效果猜测是refreshControl自带的
        self.refreshControl.backgroundColor = UIColor(red:0.113, green:0.113, blue:0.145, alpha:1)
        let attributes = [NSAttributedStringKey.foregroundColor: UIColor.white]
        // 这里是loading下方的文案
        self.refreshControl.attributedTitle = NSAttributedString(string: "Last updated on \(Date())", attributes: attributes)
        self.refreshControl.tintColor = UIColor.white
        
        self.title = "emoji"
        self.navBar.barStyle = UIBarStyle.blackTranslucent
        
        emojiTableView?.rowHeight = UITableViewAutomaticDimension
        emojiTableView?.estimatedRowHeight = 60.0
        emojiTableView?.tableFooterView = UIView(frame: CGRect.zero)
        emojiTableView?.separatorStyle = UITableViewCellSeparatorStyle.none
        
        // 代码add子View
        self.view.addSubview(emojiTableView!)
        self.view.addSubview(navBar)
    }

这里通过在代码里面new一个UITableView方式,也是可以实现UI效果。

7.4 设置代理和数据源

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return emojiData.count
    }
    
func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifer)! as UITableViewCell
    cell.textLabel!.text = self.emojiData[indexPath.row]
    cell.textLabel!.textAlignment = NSTextAlignment.center
    cell.textLabel!.font = UIFont.systemFont(ofSize: 50)
    cell.backgroundColor = UIColor.clear
    cell.selectionStyle = UITableViewCellSelectionStyle.none

    return cell
}

这里给Cell设置数据了。

7.5 下拉监听

@objc func didRoadEmoji() {
    DispatchQueue.main.asyncAfter(deadline:DispatchTime.now() + 3 ) {
        self.emojiData = [self.newFavoriteEmoji,self.favoriteEmoji][self.index]
        self.tableView.reloadData()
        self.refreshControl.endRefreshing()
        self.index = (self.index + 1) % 2
    }
}

DispatchQueue.main.asyncAfter: 主线程执行延迟任务方法。
这里延迟3s后,去更新数据。然后控制器取消刷新。

8 炫彩音乐

8.1 效果

8.2 全局变量定义

var audioPlayer = AVAudioPlayer()
    
let gradientLayer = CAGradientLayer()

var timer : Timer?

var backgroundColor: (red: CGFloat, green: CGFloat,blue: CGFloat,alpha: CGFloat)! {
    didSet {
        let color1 = UIColor(red: backgroundColor.blue,
                                green: backgroundColor.green,
                                blue: 0,
                                alpha: backgroundColor.alpha).cgColor
        let color2 = UIColor(red: backgroundColor.red,
                                green: backgroundColor.green,
                                blue: backgroundColor.blue,
                                alpha: backgroundColor.alpha).cgColor
        gradientLayer.colors = [color1, color2]
    }
}

这里第一个是AVAudioPlayer,负责音频播放。
这里第二个类是CAGradientLayer,负责渐变色渲染。
timer是定时器。
backgroundColor背景色,如果任何地方更改后,触发渐变色颜色变化。

8.3 播放音乐

@IBAction func playMusicButtonDidTouch(_ sender: AnyObject) {
        
    //play bg music
    let bgMusic = URL(fileURLWithPath: Bundle.main.path(forResource: "Ecstasy", ofType: "mp3")!)
    
    do {
        try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
        try AVAudioSession.sharedInstance().setActive(true)
        try audioPlayer = AVAudioPlayer(contentsOf: bgMusic)
        
        audioPlayer.prepareToPlay()
        audioPlayer.play()
        
    }
    catch let audioError as NSError {
        print(audioError)
    }
    
    if (timer == nil) {
        timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(ViewController.randomColor), userInfo: nil, repeats: true)
    }
    
    let redValue = CGFloat(drand48())
    let blueValue =  CGFloat(drand48())
    let greenValue = CGFloat(drand48())
    
    self.view.backgroundColor = UIColor(red: redValue, green: greenValue, blue: blueValue, alpha: 1.0)
    
    //graditent color
    gradientLayer.frame = view.bounds
    gradientLayer.startPoint = CGPoint(x: 0, y: 0)
    gradientLayer.endPoint = CGPoint(x: 1, y: 1)
    
    self.view.layer.addSublayer(gradientLayer)
    
}

这里先利用AVAudioSession的单例类设置Active,然后利用AVAudioPlayer播放本地音乐。

然后后面搞了0.2s刷新的周期定时器。每次会执行这个函数:

@objc func randomColor() {
    
    let redValue = CGFloat(drand48())
    let blueValue =  CGFloat(drand48())
    let greenValue = CGFloat(drand48())
    
    
    backgroundColor = (redValue, blueValue, greenValue, 1)
    
}

这里红蓝绿会随机获取一个值,生成一个颜色值。同时会更新渐变层变更,这样背景就随机变化了。

9 图片缩放效果

9.1 效果

9.2 布局构造

这里看了下Main.storyboard
貌似是在ScrollView里面放了一个ImageView哦。

这里有个细节,就是背景有点毛玻璃效果。
其实是这样的,它在Main.storyboard底部设置了一张UIImageView,其实就是这张图片。
然后再搞了一个UIVisualEffectView覆盖再上面,就形成了一种毛玻璃效果。

回到控制器里面,这里无非就是两个视图,并且把这个Constraint约束也加进来了,有4个约束,顶部约束,头部约束,底部约束和尾部约束。

@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!

9.3 代码实现

首先在初始化里面配置了一个mask:

 scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

这个是自动调整尺寸设置。

其实ScrollView本来就支持缩放的:

在iOS中,滚动视图UIScrollView用于查看大于屏幕的内容。Scroll View有两个主要目的:
让用户拖动视图以显示更多内容区域。
让用户使用捏合手势放大或缩小所显示的内容。

这里需要关注一个生命周期函数:viewWillLayoutSubviews

有以下几种情况会调用(init初始化不会触发layoutSubviews)
1、addSubview会触发viewWillLayoutSubviews
2、设置self.view及子视图的frame.size会触发layoutSubviews,当然前提是frame.size的值设置前后发生了变化,注意,此处不是origin,呼应官方文档上的边界发生变化
3、滚动一个UIScrollView(该scrollview有子视图的时候)会触发layoutSubviews
4、横竖屏幕切换会触发

这里最为关键的就是这个 viewWillLayoutSubviews函数和viewForZooming了,如果没有这个代码,无法支持缩放效果!

//每次控制器更新其子视图时,更新最小缩放比例
override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    updateMinZoomScaleForSize(view.bounds.size)
}

//计算scrollView的缩放比例,缩放比例为1表示内容以正常大小显示;缩放比例小于1表示容器内的内容缩小,缩放比例大于1表示放大容器内的内容
fileprivate func updateMinZoomScaleForSize(_ size: CGSize)
{
    //要获得最小的缩放比例,首先计算所需的缩放比例,以便根据其宽度在scrollView中紧贴imageView
    let widthScale = size.width / imageView.bounds.width
    let heightScale = size.height / imageView.bounds.height
    //选取宽度和高度比例中最小的那个,设置为minimumZoomScale
    let minScale = min(widthScale,heightScale)
    
    scrollView.minimumZoomScale = minScale
    scrollView.maximumZoomScale = 3.0
    scrollView.zoomScale = minScale
}

这样ScrollView就有一个最小和最大缩放比例了。

另外还有一个协议:

func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    //当手势动作发生时,scrollView告诉控制器要放大或缩小子视图imageView
    return imageView
}

必须要有viewForZooming,这个作用就是谁可以放大或缩小。

viewForZooming+viewWillLayoutSubviews,两者结合才能实现子View缩放效果。

两位还有一个scrollViewDidZoom方法, 当scrollView缩放时调用,在缩放过程中会被多次调用:

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    updateConstraintsForSize(view.bounds.size)
}

//当scrollView的内容大小小于边界时,内容将放置在左上角而不是中心,updateConstraintForSize方法处理这个问题;通过调整图像视图的布局约束。
fileprivate func updateConstraintsForSize(_ size: CGSize) {
    
    ////将图像垂直居中,从视图高度减去imageView的高度并分成两半,这个值用作顶部和底部imageView的约束
    let yOffset = max(0, (size.height - imageView.frame.height) / 2)
    imageViewTopConstraint.constant = yOffset
    imageViewBottomConstraint.constant = yOffset
    
    ////根据宽度计算imageView前后约束的偏移量
    let xOffset = max(0, (size.width - imageView.frame.width) / 2)
    imageViewLeadingConstraint.constant = xOffset
    imageViewTrailingConstraint.constant = xOffset
    
    view.layoutIfNeeded()
}

另外还有其它的代理方法如下:

func scrollViewDidScroll(_ scrollView: UIScrollView)
scrollView滚动时调用,在滚动过程中会多次调用

func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
将要开始拖拽时调用

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer)
将要停止拖拽时 velocity:加速度 向左滑动 x为负值,否则为正值 向上滚动为y为负值否则为正值;targetContentOffset:滚动停止时的ContentOffset

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
停止拖拽时调用, willDecelerate:停止拖拽时是否要减速,若值为false表示已经停止减速,也就意味着滚动已停止,此时不会调用scrollViewWillBeginDecelerating和scrollViewDidEndDecelerating;若值为true,则代表scrollView正在减速滚动

func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView)
开始减速的时候调用(也就是松开手指时),在拖拽滚动的时候,如果松手时已经停止滚动则不会调用

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
停止减速的时候调用(也就是停止滚动的时候调用),在拖拽滚动的时候,如果松手时已经停止滚动则不会调用

func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
当调用setContentOffset(_ contentOffset: CGPoint, animated: Bool)/scrollRectToVisible(_ rect: CGRect, animated: Bool)API并且animated参数为true时,会在scrollView滚动结束时调用。若是UITableView或者UICollectionView,调用scrollToRow也和上面一样

func viewForZooming(in scrollView: UIScrollView) -> UIView?
放回要缩放的view,此view必须是scrollView的subview

func scrollViewDidZoom(_ scrollView: UIScrollView)
当scrollView缩放时调用,在缩放过程中会被多次调用

func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?)
scrollView开始缩放时调用

func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
scrollView结束缩放时调用

func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool
是否允许点击scrollview的头部,让其滚动到最上面,若不实现此代理,则默认为true

func scrollViewDidScrollToTop(_ scrollView: UIScrollView)
当滚动到最上面时调用

10 视频背景

10.1 效果

10.2 定义一个基类播放控制器

public enum ScalingMode {
  case resize
  case resizeAspect
  case resizeAspectFill
}

定义了3种缩放模式。

再定义有关视频的属性:

// 视频播放器
fileprivate let moviePlayer = AVPlayerViewController()
     
  // 声音   
  fileprivate var moviePlayerSoundLevel: Float = 1.0

   // 播放url  
   var contentURL: URL? {
    didSet {
      if let _contentURL = contentURL {
      setMoviePlayer(_contentURL)
      }
    }
  }

   var videoFrame: CGRect = CGRect()
   var startTime: CGFloat = 0.0
   var duration: CGFloat = 0.0

   // 背景
   var backgroundColor: UIColor = UIColor.black {
    didSet {
      view.backgroundColor = backgroundColor
    }
  }

  // 声音
   var sound: Bool = true {
    didSet {
      if sound {
        moviePlayerSoundLevel = 1.0
      }else{
        moviePlayerSoundLevel = 0.0
      }
    }
  }
  
  // alpha
   var alpha: CGFloat = CGFloat() {
    didSet {
      moviePlayer.view.alpha = alpha
    }
  }

  // 是否重复 这里发送一个全局通知
   var alwaysRepeat: Bool = true {
    didSet {
      if alwaysRepeat {
        NotificationCenter.default.addObserver(self,
          selector: #selector(VideoSplashViewController.playerItemDidReachEnd),
          name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
          object: moviePlayer.player?.currentItem)
      }
    }
  }

  // 填充模式
   var fillMode: ScalingMode = .resizeAspectFill {
    didSet {
      switch fillMode {
      case .resize:
          moviePlayer.videoGravity = AVLayerVideoGravity.resize.rawValue
      case .resizeAspect:
          moviePlayer.videoGravity = AVLayerVideoGravity.resizeAspect.rawValue
      case .resizeAspectFill:
          moviePlayer.videoGravity = AVLayerVideoGravity.resizeAspectFill.rawValue
      }
    }
  }

生命周期已经显示,添加子View和视频view:

  override func viewDidAppear(_ animated: Bool) {
    moviePlayer.view.frame = videoFrame
    moviePlayer.showsPlaybackControls = false
    view.addSubview(moviePlayer.view)
    view.sendSubview(toBack: moviePlayer.view)
  }

生命周期将要消失,移除通知:

  override func viewWillDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    NotificationCenter.default.removeObserver(self)
  }

设置播放器:

 fileprivate func setMoviePlayer(_ url: URL){
    let videoCutter = VideoCutter()
    videoCutter.cropVideoWithUrl(videoUrl: url, startTime: startTime, duration: duration) { (videoPath, error) -> Void in
      if let path = videoPath as URL? {
        DispatchQueue.main.async {
            self.moviePlayer.player = AVPlayer(url: path)
            self.moviePlayer.player?.play()
            self.moviePlayer.player?.volume = self.moviePlayerSoundLevel
        }
      }
    }
  }

播放视频,前面监听视频播放完成消息,然后走这个方法,可实现重复播放效果:

  @objc func playerItemDidReachEnd() {
    moviePlayer.player?.seek(to: kCMTimeZero)
    moviePlayer.player?.play()
  }

10.3 实现视频背景效果控制器

 @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var signupButton: UIButton!

这里搞2个View。

override func viewDidLoad() {
    super.viewDidLoad()
    setupVideoBackground()
    
    loginButton.layer.cornerRadius = 4
    signupButton.layer.cornerRadius = 4
    
}

func setupVideoBackground() {
    
    let url = URL(fileURLWithPath: Bundle.main.path(forResource: "moments", ofType: "mp4")!)
    
    videoFrame = view.frame
    fillMode = .resizeAspectFill
    alwaysRepeat = true
    sound = true
    startTime = 2.0
    alpha = 0.8
    
    contentURL = url
    // view.isUserInteractionEnabled = false
    
}

这里初始化,设置视频背景,设置按钮圆角。

10.4 视频剪切工具

前面设置播放器,用到了一个VideoCutter类,
这里:

 fileprivate func setMoviePlayer(_ url: URL){
    let videoCutter = VideoCutter()
    videoCutter.cropVideoWithUrl(videoUrl: url, startTime: startTime, duration: duration) { (videoPath, error) -> Void in
      if let path = videoPath as URL? {
        DispatchQueue.main.async {
            self.moviePlayer.player = AVPlayer(url: path)
            self.moviePlayer.player?.play()
            self.moviePlayer.player?.volume = self.moviePlayerSoundLevel
        }
      }
    }
  }

这里其实是根据startTime和duration来剪切目标视频url,返回一个新的videoPath,这里再重新播放的。

需要看下如何剪切的:

extension String {
  var convert: NSString { return (self as NSString) }
}

 class VideoCutter: NSObject {

  /**
  Block based method for crop video url
  
  @param videoUrl Video url
  @param startTime The starting point of the video segments
  @param duration Total time, video length

  */
   func cropVideoWithUrl(videoUrl url: URL, startTime: CGFloat, duration: CGFloat, completion: ((_ videoPath: URL?, _ error: NSError?) -> Void)?) {
    DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
        let asset = AVURLAsset(url: url, options: nil)
        let exportSession = AVAssetExportSession(asset: asset, presetName: "AVAssetExportPresetHighestQuality")
        let paths: NSArray = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) as NSArray
        var outputURL = paths.object(at: 0) as! String
        let manager = FileManager.default
        // 异常处理:
        // try?: 将错误转化成可选异常,有错误发生时返回nil
        // try!: 禁用错误传递,当确认无异常发生时使用,否则可能会发生运行时异常
        // defer: 在即将离开当前代码块时执行一系列语句,延迟执行的语句不能包含任何控制转移语句,例如break、return语句,或是抛出一个错误。
        // defer语句是从后往前执行
        do {
            try manager.createDirectory(atPath: outputURL, withIntermediateDirectories: true, attributes: nil)
        } catch _ {
        }
        outputURL = outputURL.convert.appendingPathComponent("output.mp4")
        do {
            try manager.removeItem(atPath: outputURL)
        } catch _ {
        }
        if let exportSession = exportSession as AVAssetExportSession? {
            exportSession.outputURL = URL(fileURLWithPath: outputURL)
            exportSession.shouldOptimizeForNetworkUse = true
            exportSession.outputFileType = AVFileType.mp4
            let start = CMTimeMakeWithSeconds(Float64(startTime), 600)
            let duration = CMTimeMakeWithSeconds(Float64(duration), 600)
            let range = CMTimeRangeMake(start, duration)
            exportSession.timeRange = range
           
            exportSession.exportAsynchronously {
                switch exportSession.status {
                case AVAssetExportSessionStatus.completed:
                    completion?(exportSession.outputURL, nil)
                case AVAssetExportSessionStatus.failed:
                    print("Failed: \(String(describing: exportSession.error))")
                case AVAssetExportSessionStatus.cancelled:
                    print("Failed: \(String(describing: exportSession.error))")
                default:
                    print("default case")
                }
            }
        }
    }
  }
}

这是个工具,简单看下就行。

11 总结

  • 1.第一个是计时器demo,界面很简单,主要学会Timer用法,每次继续其实也是new了一个Timer,Timer的作用只是更新,数值是我们自己记录的。
  • 2.第二个改变字体,主要学会字体文件放在根目录,xCode会自动识别,然后设置字体可以通过UILabel的Font属性,其次学会UiTableView用法,懂得设置数据源和代理。
  • 3.本地视频,当然就是学会AVPlayer的用法,还要用到系统的一个控制器。
  • 4.摄像头,主要学会Session用法,input,outPut设置,通过设置AV预览图设置给UIView,达成预览效果,另外学会水平ScrollView可实现类似分页效果,当然坐标要按照屏幕宽度设置下。
  • 5.传送带效果,学会设置UICollectionView和UIScrollView的代理,做到良好的用户体验效果,滑动到半屏和不足半屏的考虑。另外学会了xib布局方式,可以绑定控制器。
  • 6.定位,学会使用系统提供的定位Api,在代理里面处理自己的逻辑。
  • 7.下拉刷新,是UIScrollView里面的refreshControl提供的功能,自带了loading效果,可以自行添加文案。
  • 8.炫彩音乐,主要学会音频播放,以及随机函数的用法,还有渐变色调用方式。
  • 9.图片缩放效果,这个主要是UIScrollView的子View这种方式提供的缩放效果,主要还是针对代理方法做一些调整。
  • 10.视频背景,这个主要就是学会构造一个基类控制器,可实现播放视频效果,通过配置不同参数丰富这个基类,然后另外就是学会视频剪切。

   转载规则


《iOS swift 3天30个Swift项目之第一天》 Jason 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
iOS swift 3天30个swift项目之第二天 iOS swift 3天30个swift项目之第二天
1 渐变TableView1.1 效果 1.2 代码实现直接是继承了UITableViewController这个控制器,这个应该是自带了一个列表。内部有一个tableView。 class ClearTableViewControlle
2023-02-04
下一篇 
iOS swift 打造TabLayout效果 iOS swift 打造TabLayout效果
1 需求定义在首页,我们需要实现一个多tab页,支持左右滑动,上滑吸顶的效果。比如这样: 需要保证良好的用户体验效果,每个页面都是独立的,可以上滑吸顶,左右滑动页面切换,而且还需要支持未读数字提示。 2 需求分析针对这些需求,github上
2023-02-02
  目录