75142913在线留言
CS193p2021学习笔记第九课:新项目EmojiArt(手势拖放及多线程)_IOS开发_网络人

CS193p2021学习笔记第九课:新项目EmojiArt(手势拖放及多线程)

Kwok 发表于:2021-08-26 10:26:28 点击:3 评论: 0

从本课开始使用新的项目演示,一个可以拖放图片及Emoji表情到画板上的APP,本项目学习的知识主要有各种手势、多线程、远程数据下载等。

一、课前理论知识

1、Collection 与 Array、Set等的关系

在前面的课程中我们 extension Array 为Array增加了一个oneAndOnly功能,假如我们要用Set也增加一个,我们是不是还需要extension Set呢,课程一开始就举例使用extension Collection 为所有符合此协议的类型增加功能:

//符合Identifiable协议的集合都可以调用此方法
extension Collection where Element: Identifiable{
   //查找索引的功能
   func index(matching element: Element) -> Self.Index? {
       firstIndex(where: { $0.id == element.id })//返回找到的索引
       //remove(at: index)
   }
}

像这种在协议上扩展功能的情况今后在大型开发中会经常使用到,但是返回的Self.Index 而不是Int,这里需要注意的是因为我们直接给协议增加的扩展功能,我需要返回的是索引(Index),Selft代表的是具体类型(Array、Set、String等由Self表示)。比如String的索引就不能使用Int来表示,你必须使用一个String.index的。String不符合Identifiable,所以上面的功能它并不能用。

2、为集合增加一个删除功能

我们不同直接扩展Collection为集合增加删除功能,我们需要为RangReplaceableCollection增加因为它有一个remove的内置功能,Array、Set都可以使用。

//符合Identifiable协议的集合都可以调用此方法
extension RangReplaceableCollection where Element: Identifiable{
   mutating func remove(_ element: Element){
       //通过上面的index方法找到索引并删除索引项
       if let index = index(matching: element){
           remove(at: index)//删除找到的索引项   
       }
   }
}

RangReplaceableCollection 继承于Collection,因为Collection本质上是不可变的协议,而RangReplaceableCollection的出现就是为了弥补这个问题。所以RangReplaceableCollection是可以被修改、删除的。

3、Color颜色(8:53)

我们前几课演示中已多次使用了产生,除了可以使用Color.red、.blue这样的,我们还使用了Color.clear这个特殊颜色。应用颜色的视频修改器一般有 .foregroundColor(Color.green)、图形填充.fill(Color.orange)、在View直接使用Color.white等情况。

4、UIColor (9:41)

和Color不同的是UIColor有更多操作的API,可以知道 颜色值、色调、饱和度、亮度值等信息。我们可以通过UIColor获取并对其进行调整。而我不能使用Color做到因为它没有任何的API,这里就体现出了UIColor的优势与灵活性。

我们可以使用Color(uiColor:)初始化器将UIColor变成一个Color,我们谈论的这几种颜色都是可以相互转换的。

5、CGColor(10:34)

以CG开头都是系统核心图形参数(Core Graphic), CGColor是系统中的基本颜色表示。就像CGPoint、CGFloat、CGSize一样。其API不多,我们主要使用color.cgColor的Color变量从Color转为CGColor。而且这是一个可选类型(optional),不能保存100%成功。

CGColor可以转为UIColor然后又可以转回Color。所以它们几个都可以通过直接或者间接的方法转换的。

6、Image 图像(13:00)

Image主要是一个View,它并不是包含图像的变量类型,它也不存储任何的图像类型。Image可以通过在Assets.xcassets中访问所知道的图像 Image(_ name: String)

我们也可以使用Image(systemName: ) 获取SF Symbol里的图像。图像使用说明:http://www.neter8.com/ios/136.html

7、UIImage(14:47)

与Image不同的是UIImage是存储图像的东西,它实际上是一个JPEG或者TIFF或者任何图像的实体。可以缩放、调整大小修改等。如果要在视图里显示UIImage我们需要通过 image(uiImage:) 调用。它们的关系有点类似Color与UIColor一样。所以,如果你想保存一个图片的变量就需要使用UIImage,如果你要在UI绘制或者是展示一个图像就要使用Image。

8、对将要使用到的OC代码简单介绍(17:20):以NS开头的要使用到的早期接口的说明。NSAttributedString 、NSString、NSURL、NSItemProvider等。

二、数据结构设计(25分钟处开始)

首先是新建一个项目,命名为EmojiArt,然后新建Model文件名:EmojiArtModel.swift,然后对其数据结构定义做了详细解释。

1、Model文件:

import Foundation
//EmojiArtDocment是独立于UI与屏幕的,没有引入SwiftUI,所以我们不能使用CG开头的值
struct EmojiArtModel {
    //EmojiArt文档只有背影与表情这2个部分
    var background = Background.blank//默认为空背影,值来源枚举定义
    var emojis = [Emoji]()//文档里拖入所有多表情包数组
    
    //单个表情信息,外部可以使用类型,但只能本文档才可以初始化以保护数据安全
    struct Emoji:Identifiable,Hashable {
        //今后要扩展可以直接修改本结构,如旋转表情,透明度等
        let text: String//let 表情内容
        /* 视频26:18解释为什么不使用CGFloat这类的值 */
        var x: Int//坐标x
        var y: Int//坐标y
        var size: Int//尺寸
        let id: Int//id 使用let不可变
        
        //自己定义的初始化使用fileprivate表示只能可以让本文档内的EmojiArtModel使用
        fileprivate init(text: String,x: Int, y: Int, size: Int, id: Int){
            self.text = text
            self.x = x
            self.y = y
            self.size = size
            self.id = id
        }
    }
    
    init() { }//使用这个空的初始意味部外部只能使用EmojiArtModel()初始化以保护数据安全
    
    private var uniqueEmojiId = 0//表情ID初始化
    mutating func addEmoji(_ text:String, at location:(x: Int, y: Int), size: Int){
        uniqueEmojiId += 1//按照拖入顺序定义id
        //将Emoji拖入到文档里后,文档会根据其实坐标、大小等信息确定其在文档的位置样式等。
        emojis.append(Emoji(text: text, x: location.x, y: location.y,size: size, id: uniqueEmojiId))
    }
}

2、Model依赖的enum文件:EmojiArtModel.Background.swift

import Foundation
//之所在将enum Background用文档独立出来,是因为后面会在时面使用大量的代码,如远程下载、读取本地等
extension EmojiArtModel{
    //背影只有3个选项,要么是空白,要么来源URL的图片、本地存储的图片数据
    enum Background {
        case blank//无背影
        case url(URL)//背影来源的URL
        case imageData(Data)//被保存的图片(JPEG、PNG等被数据化后)
        
        //处理url的方式,类似Optional处理方式
        var url:URL?{
            switch self {
                //如果url里有值(使用let url判断的)则返回
                case .url(let url): return url
                //默认返回nil
                default: return nil
            }
        }
        //和上面URL做了同样的事
        var imageData:Data?{
            switch self {
                case .imageData(let data): return data
                default:  return nil
            }
        }
    }
}

3、ViewModel 文件:EmojiArtDocument.swift

import SwiftUI

class EmojiArtDocument:ObservableObject {
    @Published private(set) var emojiArt:EmojiArtModel
    
    init() {
        emojiArt = EmojiArtModel()
    }
    var emojis:[EmojiArtModel.Emoji]{ emojiArt.emojis }
    var background:EmojiArtModel.Background { emojiArt.background }
    
    // MARK: - Intent(s) 用户意图
    
    //设置背影
    func setBackground(_ background: EmojiArtModel.Background) {
        emojiArt.background = background
    }
    
    //增加表情
    func addEmoji(_ emoji: String, at location: (x: Int, y: Int), size: CGFloat) {
        emojiArt.addEmoji(emoji, at: location, size: Int(size))//使用Model里的addEmoji
    }
    //移动表情
    func moveEmoji(_ emoji: EmojiArtModel.Emoji, by offset: CGSize) {
        //通过索引找到表情
        if let index = emojiArt.emojis.index(matching: emoji) {
            emojiArt.emojis[index].x += Int(offset.width)//修改移动后的x值
            emojiArt.emojis[index].y += Int(offset.height)//修改移动后的y值
        }
    }
    //缩放表情
    func scaleEmoji(_ emoji: EmojiArtModel.Emoji, by scale: CGFloat) {
        //通过索引找到表情
        if let index = emojiArt.emojis.index(matching: emoji) {
            //重新设置被缩放后的size值
            emojiArt.emojis[index].size = Int((CGFloat(emojiArt.emojis[index].size) * scale).rounded(.toNearestOrAwayFromZero))
        }
    }
}

三、扩展文件

这里的扩展文件是课前早已准备好的。主要是一些算法、语法糖和小组件。我们需要仔细阅读代码,了解为什么需要这些算法,和这些算法可以给我们带来什么样的好处。

1、ViewModel的扩展依赖文件:UtilityExtensions.swift

这个文件里有很多依赖的算法,在后面的课程中会使用这些算法。需要细细的去品。

//
//  UtilityExtensions.swift
//  EmojiArt
//
//  Created by CS193p Instructor on 4/26/21.
//  Copyright © 2021 Stanford University. All rights reserved.
//

import SwiftUI


//在一个可识别的集合中(Collection)
//我们通常想要找到id相同的元素
//作为我们已经在手边的可识别对象
//我们命名这个index(matching:)而不是firstIndex(matching:)
//因为我们假设某人创建了一个集合的可识别
//通常只有每个可标识事物中的一个
//(虽然没有限制它们这样做;这只是一个命名选择)
extension Collection where Element: Identifiable {
    func index(matching element: Element) -> Self.Index? {
        firstIndex(where: { $0.id == element.id })
    }
}

//当移除一个元素时,我们可以做同样的事情
//但我们必须把它添加到一个不同的协议(RangeReplaceableCollection)
//因为集合适用于不可变的集合(Collection)
//可变的是RangeReplaceableCollection
//我们不仅可以添加删除
//我们还可以添加一个下标,它接受一个元素的副本
//并使用它的可识别性下标到集合中
//这是在视图模型中创建绑定到数组的一种很棒的方法
//(因为ObservableObject中的Published变量可以通过$绑定到)
//(即使是publish变量上的变量或该变量上的下标)
//(或该变量上的下标,等等)

extension RangeReplaceableCollection where Element: Identifiable {
    mutating func remove(_ element: Element) {
        if let index = index(matching: element) {
            remove(at: index)
        }
    }

    subscript(_ element: Element) -> Element {
        get {
            if let index = index(matching: element) {
                return self[index]
            } else {
                return element
            }
        }
        set {
            if let index = index(matching: element) {
                replaceSubrange(index...index, with: [newValue])
            }
        }
    }
}

//如果你在HW5中使用Set来表示选择的表情
//那么你可能会发现这个语法糖函数是有用的

extension Set where Element: Identifiable {
    mutating func toggleMembership(of element: Element) {
        if let index = index(matching: element) {
            remove(at: index)
        } else {
            insert(element)
        }
    }
}


//字符串和字符的一些扩展
//帮助我们管理我们的表情符号串
//我们希望它们“只有表情符号”
//(如下为isEmoji)
//我们不希望他们有重复的表情符号
//(下面是withNoRepeatedCharacters)
extension String {
    var withNoRepeatedCharacters: String {
        var uniqued = ""
        for ch in self {
            if !uniqued.contains(ch) {
                uniqued.append(ch)
            }
        }
        return uniqued
    }
}

extension Character {
    var isEmoji: Bool {
        // Swift没有办法问一个字符是不是emoji
        //但它确实让我们检查我们的组件标量是否为emoji
        //很不幸的是unicode允许特定的标量(比如1)
        //被另一个标量修改成为表情符号(例如1️⃣)
        //因此标量“1”将报告isEmoji = true
        //我们不能检查第一个标量是否为emoji
        //这里的快速和肮脏的是看看标量是否至少是我们知道的第一个真正的表情符号
        //(“miscellaneous items”部分的开头)
        //或检查这是否是一个多标量unicode序列
        //(例如,一个带有unicode修饰符的1将被显示为emoji 1️⃣)
        if let firstScalar = unicodeScalars.first, firstScalar.properties.isEmoji {
            return (firstScalar.value >= 0x238d || unicodeScalars.count > 1)
        } else {
            return false
        }
    }
}

//从包含其他信息的url中提取实际的url到图像
//寻找imgurl键
// imgurl是一个“众所周知”的键,可以嵌入到一个url中,表示实际的图像url是什么
extension URL {
    var imageURL: URL {
        for query in query?.components(separatedBy: "&") ?? [] {
            let queryComponents = query.components(separatedBy: "=")
            if queryComponents.count == 2 {
                if queryComponents[0] == "imgurl", let url = URL(string: queryComponents[1].removingPercentEncoding ?? "") {
                    return url
                }
            }
        }
        return baseURL ?? self
    }
}

//添加/减去cgpoint和cgsize的方便函数
//在做手势处理时可能会派上用场
//因为我们做了很多坐标系统之间的转换
//注意LHS和RHS参数的类型类型如下所示
//因此,你可以通过CGSize的宽度和高度来偏移CGPoint

extension DragGesture.Value {
    var distance: CGSize { location - startLocation }
}

extension CGRect {
    var center: CGPoint {
        CGPoint(x: midX, y: midY)
    }
}

extension CGPoint {
    static func -(lhs: Self, rhs: Self) -> CGSize {
        CGSize(width: lhs.x - rhs.x, height: lhs.y - rhs.y)
    }
    static func +(lhs: Self, rhs: CGSize) -> CGPoint {
        CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height)
    }
    static func -(lhs: Self, rhs: CGSize) -> CGPoint {
        CGPoint(x: lhs.x - rhs.width, y: lhs.y - rhs.height)
    }
    static func *(lhs: Self, rhs: CGFloat) -> CGPoint {
        CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
    }
    static func /(lhs: Self, rhs: CGFloat) -> CGPoint {
        CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
    }
}

extension CGSize {
    //面积和我们一样大的区域的中心点
    var center: CGPoint {
        CGPoint(x: width/2, y: height/2)
    }
    static func +(lhs: Self, rhs: Self) -> CGSize {
        CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height)
    }
    static func -(lhs: Self, rhs: Self) -> CGSize {
        CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height)
    }
    static func *(lhs: Self, rhs: CGFloat) -> CGSize {
        CGSize(width: lhs.width * rhs, height: lhs.height * rhs)
    }
    static func /(lhs: Self, rhs: CGFloat) -> CGSize {
        CGSize(width: lhs.width/rhs, height: lhs.height/rhs)
    }
}

//在CGSize和CGFloat中添加RawRepresentable协议一致性
//这样就可以和@SceneStorage一起使用
//首先提供rawValue和init的默认实现(rawValue:)
//在RawRepresentable中,当有问题的东西是可编码的(CGFloat和CGSize都是)
//如果要使某个可编程的东西成为raw具象的,只需要声明它是这样的
//(它将得到RawRepresentable的默认实现)

extension RawRepresentable where Self: Codable {
    public var rawValue: String {
        if let json = try? JSONEncoder().encode(self), let string = String(data: json, encoding: .utf8) {
            return string
        } else {
            return ""
        }
    }
    public init?(rawValue: String) {
        if let value = try? JSONDecoder().decode(Self.self, from: Data(rawValue.utf8)) {
            self = value
        } else {
            return nil
        }
    }
}

extension CGSize: RawRepresentable { }
extension CGFloat: RawRepresentable { }


// NSItemProvider的方便函数(即NSItemProvider数组)
//使从提供程序加载对象的代码更简单
// NSItemProvider是来自Objective-C(即前swift)世界的一个延续
//你可以通过它的名字来判断(以NS开头)
//很不幸,处理这个API有点麻烦
//因此,我建议你接受这些loadObjects函数将工作,并继续前进
//尝试深入了解这里发生了什么是一种罕见的情况
//可能不会很有效地利用你的时间
//(尽管我肯定不会说你不应该!)
//(只是想帮你优化这个季度的宝贵时间)

extension Array where Element == NSItemProvider {
    func loadObjects(ofType theType: T.Type, firstOnly: Bool = false, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading {
        if let provider = first(where: { $0.canLoadObject(ofClass: theType) }) {
            provider.loadObject(ofClass: theType) { object, error in
                if let value = object as? T {
                    DispatchQueue.main.async {
                        load(value)
                    }
                }
            }
            return true
        }
        return false
    }
    func loadObjects(ofType theType: T.Type, firstOnly: Bool = false, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading {
        if let provider = first(where: { $0.canLoadObject(ofClass: theType) }) {
            let _ = provider.loadObject(ofClass: theType) { object, error in
                if let value = object {
                    DispatchQueue.main.async {
                        load(value)
                    }
                }
            }
            return true
        }
        return false
    }
    func loadFirstObject(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading {
        loadObjects(ofType: theType, firstOnly: true, using: load)
    }
    func loadFirstObject(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading {
        loadObjects(ofType: theType, firstOnly: true, using: load)
    }
}

2、View会使用到的小组件:UtilityViews.swift 

//
//  UtilityViews.swift
//  EmojiArt
//
//  Created by CS193p Instructor on 4/26/21.
//  Copyright © 2021 Stanford University. All rights reserved.
//

import SwiftUI

//语法确保能够传递一个可选的UIImage给Image
//(通常它只接受一个不可选的UIImage)
struct OptionalImage: View {
    var uiImage: UIImage?
    
    var body: some View {
        if uiImage != nil {
            Image(uiImage: uiImage!)
        }
    }
}


//语法糖
//很多时候我们想要一个简单的按钮
//只使用文本或标签或systemImage
//但我们希望它执行的动作是动画的
//(即withAnimation)
//这只是使它容易创建这样一个按钮
//从而清理我们的代码
struct AnimatedActionButton: View {
    var title: String? = nil
    var systemImage: String? = nil
    let action: () -> Void
    
    var body: some View {
        Button {
            withAnimation {
                action()
            }
        } label: {
            if title != nil && systemImage != nil {
                Label(title!, systemImage: systemImage!)
            } else if title != nil {
                Text(title!)
            } else if systemImage != nil {
                Image(systemName: systemImage!)
            }
        }
    }
}

//简单的结构,使它更容易显示可配置的警报
//只是一个可识别的结构体,它可以根据需要创建一个Alert
//使用。alert(item: $alertToShow) {theIdentifiableAlert in…}
// alertToShow是一个绑定?
//当你想显示一个警告
//设置alertToShow = IdentifiableAlert(id: "my alert") {alert (title:…)}
//当然,字符串标识符对于所有不同类型的警报必须是唯一的

struct IdentifiableAlert: Identifiable {
    var id: String
    var alert: () -> Alert
}

四、View视图建设

上面进行了数据建模与扩展应用,现在我们开始着手视图部分的编码,首先将默认的ContentView修改为EmojiArtDocumentView。

import SwiftUI

struct EmojiArtDocumentView: View {
    //从ViewModel获取要更新的值
    @ObservedObject var docment:EmojiArtDocument
    var body: some View {
        VStack{
            docmentBody//画布主体
            palette//可选择的表情
        }
    }
    //先使用一个黄色填充画布
    var docmentBody: some View{
        Color.yellow//这里将使用ForEach遍历Model里的表情并根据信息展示出来
    }
    //使用横向滚动视图展示测试表情
    var palette: some View{
        ScrollingEmojisView(emojis: testEmojis)
    }
    let testEmojis = "😀😷🦠💉👻👀🐶🌲🌎🌞🔥🍎⚽️🚗🚓🚲🛩🚁🚀🛸🏠⌚️🎁🗝🔐❤️⛔️❌❓✅⚠️🎶➕➖🏳️"
}
//横向滚动视图
struct ScrollingEmojisView:View {
    let emojis:String
    var body: some View{
        ScrollView(.horizontal){
            HStack{
                //emojis.map是学习知识点
                //通过map{ $0 }将字符串映射成一个字符串数组
                //let $0: String.Element所以需要String($0)
                ForEach(emojis.map{ String($0) },id:.self){ emoji in
                    Text(emoji)
                }
            }
        }
    }
    
}

五、开始在画布展示表情

通过上面的一系列工作,我们将ForEach在docmentBody上展示表情。

1、在ViewModel里的init增加一行测试用的表情

emojiArt.addEmoji("👻", at: (0,0), size: 30)//坐标0,0表示放到中心

2、在画布上展示表情:

//画布
var docmentBody: some View{
    GeometryReader { geometry in
        ZStack{
            Color.yellow
            ForEach(docment.emojis){ emoji in
                Text(emoji.text)//显示表情
                    //应用size
                    .font(.system(size: fontSize(for:emoji)))
                    //应用位置信息
                    .position(position(for:emoji, in: geometry))
            }
        }}
}
//从emoji里获取尺寸并转换为CGFloat
private func fontSize(for emoji:EmojiArtModel.Emoji) -> CGFloat{
    CGFloat(emoji.size)
}
//从emoji读取位置信息并转化值
private func position(for emoji: EmojiArtModel.Emoji, in geometry: GeometryProxy) -> CGPoint {
    convertFromEmojiCoordinates((emoji.x, emoji.y), in: geometry)//返回转换后的坐标以定位表情在画布的位置
}
//将表情坐标转换成CGPoint值
private func convertFromEmojiCoordinates(_ location: (x: Int, y: Int), in geometry: GeometryProxy) -> CGPoint {
    let center = geometry.frame(in: .local).center //来源于geometry的中心点(能过扩展代码extension CGRect实现)
    return CGPoint(
        x: center.x + CGFloat(location.x) , //坐标x以中心x出发
        y: center.y + CGFloat(location.y)//坐标y以中心y出发
    )
}

看似通过ForEach遍历VM里的数据,并通过Text展示出来,而修饰Text的只有2个修改器,下面的一串函数都是用于配合这2个修饰器的。

相对于难点在于将convertFromEmojiCoordinates理解明白就行。一步一步解开convertFromEmojiCoordinates,基麻烦的地方在于第一行获取中心点坐标的方式。因为涉及到扩展和对frame的熟悉。所以可能需要查阅其它资料和文档。

六、Drag and Drop 拖拽(1:02:00)

现在我们已将表情能正常的显示到画布上了,我们下一步就要来学习怎样将表情栏里的表情通过拖拽手势添加到画布上去。

1、为表情增加.onDrag方法(拿起)

目前swift没有拖拽的功能,我们需要借助OC的代码来使用。在ScrollingEmojisView里的Text(emoji)下增加以下代码:

Text(emoji)//我们要拖拽的是单个表情,可以在这下去增加方法
     onDrag { NSItemProvider(object: emoji as NSString) }
//来自OC的代码都需要使用as 转换数据。这里将swift里的String转为OC里的NSString

NSItemProvider用于在拖放或复制/粘贴活动期间在进程之间传递数据或文件,或从宿主应用程序到应用程序扩展程序(跨程序的,后面会演示从Safari拖放图片到画布上)。 

2、为画布增加 .onDrop方法(放下)

//of里放入可放下的类型,这里有String,URL和图片都能放下
.onDrop(of: [.plainText,.url,.image], isTargeted: nil) { providers, location in
    drop(providers: providers, at: location, in: geometry)//通过drop调用VM里的增加表情功能
}

我们再写1个drop将放下的表情追回到Model的数组里就能在屏幕上显示了。

3、使用drop为ViewModel追回数据

private func drop(providers: [NSItemProvider], at location: CGPoint, in geometry: GeometryProxy) -> Bool {
    return providers.loadObjects(ofType: String.self) { string in
        if let emoji = string.first, emoji.isEmoji {
            document.addEmoji(
                String(emoji),
                at: convertToEmojiCoordinates(location, in: geometry),
                size: defaultEmojiFontSize
            )
        }
    }
}

前面写了一个convertFromEmojiCoordinates将来算Model里的表情坐标转为可显示的坐标,这里需要一个convertToEmojiCoordinates将拖进来的表情转为可写入的坐标:

private func convertToEmojiCoordinates(_ location: CGPoint, in geometry: GeometryProxy) -> (x: Int, y: Int) {
    let center = geometry.frame(in: .local).center
    let location = CGPoint(
        x: (location.x - center.x),
        y: (location.y - center.y)
    )
    return (Int(location.x), Int(location.y))
}

然后在顶部定义一个默认的表情大小为40

let defaultEmojiFontSize: CGFloat = 40//表情拖入时的默认大小

上面代码里除了理解convertToEmojiCoordinates,如果前面的理解了。这里就直接过,最麻烦的是去理解 drop 参数:NSItemProvider。

从iOS 11开始,项目提供者在拖放和复制/粘贴中发挥着核心作用。它们继续在应用程序扩展中发挥作用。

NSItemProvider类中使用的所有完成块都由系统在内部队列上调用。当使用带有拖放功能的项提供程序时,确保用户界面更新在主队列上进行(异步的)。

七、多线程理论知识(1:14:27)

与拖入表情不同的是,背影图片需要来源于网络,所以就涉及到了从网上下载图片到本地的情况。下载过程根据网络情况几秒、几十秒不等。下载期间可能会阻止UI响应,这里我们就需要使用多线程技术了 。让图片在后台下载。

1、永远不要阻止UI:对于移动应用程序的UI来说,永远不能停止响应。我们要杜绝一切因为程序响应导致用户界面卡死的情况。所以我们要使用后台线程去处理阻止用户UI的事务。

2、线程(1:16:25):大多数据的系统都是支持多线程的,线程是通过系统分配与调度在各任务之间快速切换以达到同时执行的效果。线程也是分为等级的,根据执行的级别不同执行的效果有所区别。

3、队列(1:17:35):一堆要需要执行的任务排队的代码块。由系统调度分配执行的线程处理。我们要做的就是讲代码块正确的放到队列中,其它的交给系统处理。在swift里,我们只需要将要执行的代码放入各种等级队列中(分为下面的主队列与后台队列)即可。

4、主队列(1:19:20):这是swift中最重要的队列,在上面执行任务会阻止UI响应。但任何时候我们想在UI上做一些事情必须使用Main Queue主队列,所以就存在了当后台队列处理完下载图片后,会切换到主队列上执行让图片在UI上显示的情况。所以在主队列之外直接或者间接地做UI是错误的,所有的UI活动必须要在主队列中执行。系统只需要1个主线程来处理其队列上的事务。主线程会根据处理队列按顺序依次执行。根据这个特性,我们可以使用主队列来做为同时点使用(前提是执行时间不要过长)。

5、后台队列(1:20:35):和主线程不同的是,我们通常使用后台队列执行长期的非UI任务,例如从网络下载数据、图片等。后台队列是可实现多个任务并行运行(多线程、多任务)。多线程由系统调度,我们只需要和主队列一样将需要执行的代码块放入即可。后台队列执行可以定义优先级,将在下面进行说明。此队列的优点是在执行期间不会影响UI和主线程,主线程永远享有更好的优先运行等级。

6、GCD Grand Central Dispatch (1:22:40):中央调度系统的作用是将我们的代码块分配到需要执行的线程队列上面。GCD API有2个基本任务,a.获取队列。 b.将代码块放入到队列。

7、创建队列(1:22:30):

a.使用DispatchQueue.main 将代码放到主队列上执行。

b.使用DispatchQueue.global(qos:QoS) 在后台队列上执行,qos 分为:.userInteractive(用户互动级为仅次于主线程等级,是与用户交互实时进行的,当用户拖动需要在后台执行的任务)、.userInitiated(用户触发级 一般用于用户点击了某个按钮需要执行的任务,点击下载、更新、传输等操作)、.utility(实用等级 一般用于清理缓存,处理中低等级的任务)、.background(后台等级 最低的等级,用于自动任务,定时任务等后台自动处理的任务)

8、同步与异步的区别 (1:25:30):

let queue = DispatchQueue.main 或者 DispatchQueue.global(qos:QoS)
queue.async { ... }//异步,只是将代码块放入队列中并立即返回,是否执行由后台调度
queue.sync { ... } //同步,代码执行完成后才会返回,并会阻止你从中调用它的当前执行线程,我们几乎不会在主队列中运行同步代码。

我们将在后台下载远程图片的演示中使用到后台队列并异步下载,关于更多的解释请查看GCD的文档和队列 http://www.neter8.com/ios/140.html

八、课后总结

本课内容无论是需要理解的理论还是代码都比较多,难度也相当的大,需要反复思考与阅读代码。因为使用了一点点OC代码,所以我们只能死记使用的方式。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:http://www.neter8.com/ios/154.html
标签:cs193p多线程拖放手势Kwok最后编辑于:2021-08-28 11:28:18
0
感谢打赏!

《CS193p2021学习笔记第九课:新项目EmojiArt(手势拖放及多线程)》的网友评论(0)

本站推荐阅读

热门点击文章