斯坦福2020SwiftUI课程(一) -- MVVM搭建MemoryGame

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类型对比就一目了然

StructClass
数值类型引用类型
使用时复制一份指针指向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)

为了让文本显示在最外层,需要确定调用顺序

  1. RoundedRectangle().fill() 设置卡片背景填充颜色
  2. RoundedRectangle().stroke() 设置卡片描边颜色
  3. Text() 显示Emoji文本内容

现在再来关注正反面,上面的定义是在卡片正面效果,反面我们依据上图所示,不难发现是纯色背景,我们直接定义一个圆角矩形颜色填充即可。

  1. 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)")
	}
}
  1. 定义可变变量存储卡片,类型是数组(被约束传入的只能是Card类型);
  2. 定义选择函数,用来处理卡片选择后的操作;

定义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)
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值