MVVM架构
在OC开发中我们经常使用的开发架构模式MVC(Model-View-Controller),从SwiftUI的引入,MVVM(Model-View-ModelView)开发架构在Swift中可谓大显身手。
View
首先,我们从SwiftUI开始谈起,项目创建完成后,我们就看到了两个结构代码片段。
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello SwiftUI")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
利用SwiftUI创建界面时,由于View这一层是显示程序界面,不需要被其他指针引用,因此,我们将不需要指针引用的类型选择Struct,而不是Class。事实上,SwiftUI开发中,推荐使用Struct。Struct相对于Class类型更加安全。
我们关注一下Struct和Class类型对比就一目了然
Struct | Class |
---|---|
数值类型 | 引用类型 |
使用时复制一份 | 指针指向class引用 |
拷贝写入 | ARC机制(当有对象引用,计数器加一) |
功能函数编程 | 面向对象编程 |
不能被继承 | 单继承 |
无条件初始化所有变量 | 不能无条件初始化变量 |
必须明确指出可变性 | 始终是可变类型 |
View效果图
设计思路:
为了实现卡片初始化效果图,我们观察需求,水平方向需要4个卡片(宽度均相同),每一个卡片都是圆角矩形,且颜色为橘色(反面),正面为白色。每个卡片上都有Emoji图片,且两个成为一对卡片。
了解需求后,我们开始编码。
- ZStack设置卡片基本属性
struct CardView: View{
var isFaceUp;
var body: some View{
ZStack {
if isFaceUp{
//正面
//设置圆角矩形
//整合圆角矩形和文本框
RoundedRectangle(cornerRadius: 20).fill(Color.white)
RoundedRectangle(cornerRadius: 20).stroke(lineWidth: 3)
Text("🐶")
}else{
//反面
RoundedRectangle(cornerRadius: 20).fill()
}
}
}
}
定义一个结构体CardView,显示卡片视图,类型是View。
同上面ContentView相同,我们定义一个变量为body,作为CardView内容主体,类型为some View(类View类型)。
ZStack有趣的将屏幕作为一个Z轴方向的堆栈,在Z轴方向(屏幕延伸方向)添加控件,则是Z轴方向的堆积。
去掉card.isFaceUp,我们关注重点设置一个圆角矩形和Emoji文本内容。
RoundedRectangle(cornerRadius: 20).fill(Color.white)
RoundedRectangle(cornerRadius: 20).stroke(lineWidth: 3)
Text(card.content)
为了让文本显示在最外层,需要确定调用顺序
- RoundedRectangle().fill() 设置卡片背景填充颜色
- RoundedRectangle().stroke() 设置卡片描边颜色
- Text() 显示Emoji文本内容
现在再来关注正反面,上面的定义是在卡片正面效果,反面我们依据上图所示,不难发现是纯色背景,我们直接定义一个圆角矩形颜色填充即可。
- RoundedRectangle().fill()
- 屏幕中有四个水平方向的卡片,既然有了ZStack,我们大胆推出HStack的存在性。
HStack {
ForEach(0..<4){ index in
CardView(isFaceUp:false)
}
}
水平方式使用HStack,利用ForEach遍历0…<4(0,1,2,3)四张卡片,每张卡片,我们假设显示背面朝上。
这里面用到内联函数 index 不需要使用可以通过 _ 代替
- Card公共特性
- Card绝对不是完全贴合屏幕的,我们不难看出每个Card都有内边距;
- Card的前景颜色是橙色
- Card字体是大字体(系统自带字体小的可怜)
HStack {
ForEach(...){...}
.padding() //设置间距
.foregroundColor(Color.orange) //设置前景颜色
.font(Font.largeTitle) //设置字体
每一张卡片都是同一效果,因此,在外部统一设置即可
至此,View层的样式,基本搭建起来了,根据MVVM架构模式,View的任务是用来描绘显示Model层的内容,因此,我们需要通过Model来定义Card的特点。
Model
在进行Model层编写时,由于Model层不需要被引用,因此,通常定义为Struct类型。
设计思路: 观察程序,我们发现在这个程序中我们需要多个卡片。将卡片包存在数组中。且卡片的状态(是否朝上,是否被匹配,卡片的内容定义,程序中卡片的数量等)。
Struct MemoryGame {
var cards:Array<Card>
func choose(card:Card){
print("card chosen: \(card)")
}
}
- 定义可变变量存储卡片,类型是数组(被约束传入的只能是Card类型);
- 定义选择函数,用来处理卡片选择后的操作;
定义Card类型
有趣的是Swift支持结构体嵌套,外部调用就可以使用A.B来访问B结构体。在这里,我们把Card定义为Struct类型。
Struct A{
Struct B{
...
}
}
struct Card {
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
var id: Int
}
- 默认卡片一开始背面朝上
- 默认卡片一开始没有匹配
- 卡片内容
- 卡片唯一标识符
结构体会无条件个可变参数初始化,因此
Card(isFaceUp: ,isMatched: ,content: ,id: )等Card初始化重载方法也就成立。
ViewModel
作为链接Model和View的ViewModel,我们需要定义为Class类型。
在ViewModel层我们需要链接Model,因此,定义var model在合适不过。
定义Class类型,带来的问题也就显而易见,作为引用类型,可以被外部进行引用,故并不是最安全的做法,我们需要定义private类型来限定这个model是Class的私有属性,外部不可以访问,但是问题随之而来,既然不可以访问,那我怎么给model赋值修改它…
class EmojiMemoryGame {
private var model:MemoryGame<String>
}
- 采用“玻璃门”机制: private(set)
- 通过别的变量进行访问
访问模型
//MARK: - Access to the Model
var cards:Array<MemoryGame<String>.Card>
{
model.cards
}
这里面通过数组传入MemoryGame.Card,访问到model.cards
到这里,Xcode会报错,Class需要手动给var变量进行初始化。
private var model:MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static func createMemoryGame() -> MemoryGame<String> {
let emojiCardContent:Array<String> = ["🐶","🦁"]
return MemoryGame(numberOfPairsOfCards: 2, cardContentFactory: { pairIndex in
return emojiCardContent[pairIndex]})
}
init(numberOfPairsOfCards:Int,cardContentFactory:(Int) -> CardContent) {
cards = Array<Card>()
for pairIndex in 0..<numberOfPairsOfCards {
cards.append(Card(content: cardContentFactory(pairIndex),id: pairIndex*2))
cards.append(Card(content: cardContentFactory(pairIndex),id: pairIndex*2+1))
}
}
至此,基本逻辑完成,此时,将MVVM三层代码贴出来更好的理解。
View
struct ContentView: View {
var viewModel: EmojiMemoryGame
//var定义变量
//some View表示其状态同View相似
var body: some View {
HStack {
ForEach(0..<viewModel.cards.count){ index in
CardView(card: self.viewModel.cards[index]).onTapGesture(perform: {self.viewModel.choose(card: self.viewModel.cards[index])})
}
}
.padding() //设置间距
.foregroundColor(Color.orange) //设置前景颜色
.font(Font.largeTitle) //设置字体
}
}
//抽取表情包视图
struct CardView: View{
//判断卡片正反面
var card: MemoryGame<String>.Card
var body: some View{
ZStack {
if card.isFaceUp{
//正面
//设置圆角矩形
//整合圆角矩形和文本框
RoundedRectangle(cornerRadius: 20).fill(Color.white)
RoundedRectangle(cornerRadius: 20).stroke(lineWidth: 3)
Text(card.content)
}else{
//反面
RoundedRectangle(cornerRadius: 20).fill()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: EmojiMemoryGame())
}
}
Model
import Foundation
struct MemoryGame<CardContent> {
var cards:Array<Card>
init(numberOfPairsOfCards:Int,cardContentFactory:(Int) -> CardContent) {
cards = Array<Card>()
for pairIndex in 0..<numberOfPairsOfCards {
cards.append(Card(content: cardContentFactory(pairIndex),id: pairIndex*2))
cards.append(Card(content: cardContentFactory(pairIndex),id: pairIndex*2+1))
}
}
func choose(card:Card) {
print("card chosen:\(card)")
}
struct Card {
var isFaceUp: Bool = true
var isMatched: Bool = false
var content: CardContent
var id: Int
}
}
ViewModel
import SwiftUI
class EmojiMemoryGame {
private var model:MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static func createMemoryGame() -> MemoryGame<String> {
let emojiCardContent:Array<String> = ["🐶","🦁"]
return MemoryGame(numberOfPairsOfCards: 2, cardContentFactory: { pairIndex in
return emojiCardContent[pairIndex]})
}
//MARK: - Access to the Model
var cards:Array<MemoryGame<String>.Card>
{
model.cards
}
//MARK: - Intents(s)
func choose(card: MemoryGame<String>.Card) {
model.choose(card: card)
}
}