objc** SwiftUI与Combine编程 学习笔记,课后练习,代码
ViewModifier在Swift中的定义
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ViewModifier {
/// The type of view representing the body.
associatedtype Body : View
/// Gets the current body of the caller.
///
/// `content` is a proxy for the view that will have the modifier
/// represented by `Self` applied to it.
func body(content: Self.Content) -> Self.Body
/// The content view type passed to `body()`.
typealias Content
}
ViewModifier是SwiftUI中定义的一个协议,只有一个要求实现的方法func body(content: Self.Content) -> Self.Body
HStack {
Spacer()
Button(action: {print("star")} ){
Image(systemName: "star")
.font(.system(size: 25))
.foregroundColor(.white)
.frame(width: 30, height: 30)
}
Button(action: {print("panel")}) {
Image(systemName: "chart.bar")
.font(.system(size: 25))
.foregroundColor(.white)
.frame(width: 30, height: 30)
}
Button(action: {print("web")}){
Image(systemName: "info.circle")
.font(.system(size: 25))
.foregroundColor(.white)
.frame(width: 30, height: 30)
}
}
.padding(.bottom, 12)
上面的Image创建自定义的Modifier
struct ToolButtonModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 25))
.foregroundColor(.white)
.frame(width: 30, height: 30)
}
}
使用Modifier
Button(action: {print("star")} ){
Image(systemName: "star")
.modifier(ToolButtonModifier())
}
-
隐式动画 .animation(.default)
隐式动画作用范围很大,只要这个View甚至是它的子View上的可动画属性发生变化,这个动画就将适用。
-
显式动画 .withAnimation{ self.expanded.toggle() }
可以将改变app状态的操作放在withAnimation的闭包中,由闭包中状态变化所触发的View变化,将以动画的形式呈现
SwiftUI中的UIViewRepresentable协议提供了封装UIView的功能
这个协议要求实现两个方法:makeUIView()、updateUIView()
public protocol UIViewRepresentable : View where Self.Body == Never {
/// The type of view to present.
associatedtype UIViewType : UIView
func makeUIView(context: Self.Context) -> Self.UIViewType
func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)
associatedtype Coordinator = Void
func makeCoordinator() -> Self.Coordinator
typealias Context = UIViewRepresentableContext<Self>
}
以封装一个半透明效果的BlurView为例
创建一个BlurView,实现UIViewRepresentable协议,实现其中的makeUIView(context: Context) -> some UIView 和updateUIView(_ uiView: UIViewType, context: Context)方法,在这个例子中,只需要实现makeUIView这个方法即可,更新无需做任何操作。代码如下:
struct BlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
func makeUIView(context: Context) -> some UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: style)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(blurView)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor)
])
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) { }
}
为了便于blurview的使用,创建一个extension方法
extension View {
func blurBackground(style: UIBlurEffect.Style) -> some View {
ZStack {
BlurView(style: style)
self
}
}
}
使用:
var body: some View {
VStack(spacing: 20) {
topIndicator
Header(model: model)
pokemonDescription
Divider()
AbilityList(model: model, abilityModels: abilities)
}
.padding(EdgeInsets(
top:12,
leading: 30,
bottom: 30,
trailing: 30
))
.blurBackground(style: .systemMaterial) //使用毛玻璃效果
.cornerRadius(20)
.fixedSize(horizontal: false, vertical: true)
}
- 异步编程的本质是响应未来的事件流
- 异步操作中,某个事件发生时,将这个事件和与其相关的数据“发布”出来。对这个事件感兴趣的代码可以订阅这个事件,来进行后续的操作。
- 用户界面只是状态的函数, View = f(State)
- 将状态变化看做是被发布出来的异步操作的事件,订阅这个事件,即为SwiftUI的根据状态更新和绘制View的代码
- 需要对事件数据进行某个操作
combine中有三种重要角色:
- 负责发布事件的Publisher
- 负责订阅事件的Subscriber
- 负责转换事件和数据的Operator
在 Combine 中,我们使用 Publisher 协议来代表事件的发布者。Swift 提倡使用面 向协议编程的方式,Combine 中包括 Publisher 在内的一系列角色都使用协议来进 行定义,也正是这一**的具体体现。
publisher的定义
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where
S : Subscriber,
Self.Failure == S.Failure,
Self.Output == S.Input
}
Publisher最主要的工作其实有两个:
- 发布新的事件及其数据
- 以及准备好被Subscribe订阅
Publisher发布三种事件
- 类型为Output的新值: 代表事件流中出现了新的值
- 类型为Failure的错误:代表事件流中发生了问题,事件流到此终止。
- 完成事件:表示事件流中所有的元素都已经发布结束,事件流到此终止。
最终会终结的事件流称为有限事件流
不会发出failure或者finished的事件流称为无限事件流
- app当做一个状态机,状态决定用户界面
- 状态都保存在一个Store对象中
- View不能直接操作State,而只能通过发送Action的方式,间接改变存储在Store中的State
- Reducer接受原有的State和发送过来的Action,生成新的State
- 用新的State替换Store中原有的状态,并用新状态来驱动更新界面
SwiftUI中对传统的Redux架构进行了一些改变。
- 除了通过Action外,还可以通过Binding来改变状态。
- 在Reducer处理当前State和Action后,除了返回新State,再额外返回一个Command值,让Command来执行所需的副作用
- 防止View和Model耦合,解耦
- 将AppStae的修改分散在app各处,很快将会难以维护
- 方便测试
Shape协议是自定义绘制中最基本的部分
public protocol Shape : Animatable, View {
func path(in rect: CGRect) -> Path
}
常用的一些形状都是Shape协议的具体实现,例如Circle、Rectangle
自定义View,实现Shape协议中的path方法
例子:实现下面这个自定义的图形
//TriangleArrow.swift
import Foundation
import SwiftUI
struct TriangleArrow: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: .zero)
path.addArc(center: CGPoint(x: -rect.width / 5, y: rect.height / 2), radius: rect.width / 2, startAngle: .degrees(-45), endAngle: .degrees(45), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height / 2))
path.closeSubpath()
}
}
}
struct Demo: View {
var body: some View {
TriangleArrow()
.fill(Color.green)
.frame(width: 80, height: 80)
}
}
struct Demo_Previews: PreviewProvider {
static var previews: some View {
Demo()
}
}
结果:
SwiftUI中,可以通过GeometryReader来读取parentView提供的一些信息。
GeometryReader也是一个View,初始化方法传入一个ViewBuilder的闭包,用来构建被包装的View,这个闭包提供一个GeometryProxy的结构体
GeometryReader的定义:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct GeometryReader<Content> : View where Content : View {
public var content: (GeometryProxy) -> Content
@inlinable public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content)
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required ``View/body-swift.property`` property.
public typealias Body = Never
}
GeometryProxy:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct GeometryProxy {
/// The size of the container view.
public var size: CGSize { get }
/// Resolves the value of `anchor` to the container view.
public subscript<T>(anchor: Anchor<T>) -> T { get }
/// The safe area inset of the container view.
public var safeAreaInsets: EdgeInsets { get }
/// Returns the container view's bounds rectangle, converted to a defined
/// coordinate space.
public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
}
proxy中有个size,可以获取container view的尺寸,这让我们可以按照尺寸自适应缩放构建UI。
总体:协商解决,层层上报
具体规则:
父层级的View根据某种规则,向子View“提议”一个可行的尺寸;
子View以这个尺寸为参考,按照自己的需求进行布局:
或占满所有可能的尺寸(比如Rectangle和Circle),
或按照自己要显示的内容确定新的尺寸(比如Text),
或把这个任务再委托给自己的子View继续进行布局(比如各类StackView)。
在子View确定自己的尺寸后,将这个需要的尺寸汇报回父View,
父View最后把这个确定好尺寸的子View放置在坐标系合适的位置上。
Text并不“盲目”遵守自身内容的尺寸,而是会更多地尊重提案的尺寸,通过换行或是把内容省略为“...”来修改内容,去尽量满足提案。
通过.layoutPriority,可以控制计算布局的优先级,让父View优先对某个字View进行考虑和提案。默认情况下,布局优先级都是0,传一个更大的值就可以提高优先级。
HStack(alignment: .center) {
Image(systemName: "person.circle")
.background(Color.yellow)
Text("User:")
.background(Color.red)
Text("onevcat | Wei Wang")
.layoutPriority(1)
.background(Color.green)
}
.lineLimit(1)
.frame(width: 200)
.background(Color.purple)
fixedSize,这个modifier将提示布局系统忽略掉外界条件,让被修饰的View使用它在无约束下原本应有的理想尺寸
将fixedSize跟frame两个modifier调换顺序,布局和不加fixedSize的时候完全一致。
因为大部分View modifier所做的,并不是“改变”View上的某个属性,而是“用一个带有相关属性的新View来包装原有的View”。
frame 也不例外: 它并不是将所作用的 View 的尺寸进行更改,而是新创建一个 View,并强制地用指 定的尺寸,对其内容 (其实也就是它的子 View) 进行提案。这也是为什么将 fixedSize 写在 frame 之后会变得没有效果的原因:因为 frame 这个 View 的理想尺寸就是宽 度 200,它已经是按照原本的理想尺寸进行布局了,再用 fixedSize 包装也不会带来 任何改变。
frame方法的两种版本里,所有的参数都有默认值nil,如果使用默认值,那么frame将不在这个方向上改变原有的尺寸提案,而是将它直接传递给子View
frame中不设置width的情况下,只使用对齐,对齐将不会生效,因为内容已经占满了frame的全部空间,内容已经贴边,使用哪种对齐没有意义。