IOS开发
Lecture3
MVVM
Model-View-ViewModel
Model : UI Independent, Data + logic, “The Truth”
View : Reflects the Model, Stateless, Declared
ViewModel: Binds View to Model, Interpreter, Gatekeeper
Varieties of Types
struct & class
Both can
1.store vars
2.computed vars inline function { }
3.let vars
4.functions
func multiply(operand: Int, by: Int) -> Int{
return operand * by
}
mutiply(oprand: 5, by: 6)
//two labels
// "_" indicates none external
func multiply(_ operand: Int, by otherOperand: Int) -> Int{
return operand * otherOperand
}
multiply(5, by: 6)
5.initializers
struct RoundedRactangle {
init(cornerRadius: CGFloat){
//initialize that rectangle with that cornerRadius
}
init(cornerSize: CGSize){
//initialize this rectangle with that cornerSize
}
}
Different
struct | class |
---|---|
value type | reference type |
Copied when passed or assigned | Passed around via pointers |
Copy on write | Automatically reference counted |
Functional programming | Object-oriented programming |
No inheritance | Inheritance(single) |
“Free” init initializes ALL vars | “Free” init initializes NO vars |
Mutability must be explicitly stated | Always mutable |
Everything you’ve seen so far is a struct (except View which is a protocol) | The ViewModel in MVVM is always a class(also, UIKit is class-based) |
don’t care - generics
Swift is a strongly-typed language
We use a “don’t care” type (we call this feature “generics”)
example: Array
struct Array<Element>{
...
func append(_ element: Element) {...}
}
//Element is a "don't care" type
use like this:
var a = Array<Int>()
a.append(5)
a.append(20)
Function
(Int, Int) -> Bool
(Double) -> Void
() -> Array<String>
() -> Void
var foo: (Double) -> Void
//foo's type: function that takes a Double, returns nothing
func doSomething(what: () -> Bool)
//what's type: function, takes nothing, returns Bool
var operation: (double) ->Double
func square(operand: Double) -> Double {
return operand * operand
}
operation = square
let result = operation(4)
//result will equal to 16
Closures
inline function
private(set)
other class and struct can look at the model, but can’t change it
for
for pairIndex in 0..<numberOfPairsOfCards {
}
函数作为参数传给函数
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content))
cards.append(Card(content: content))
}
}
初始化顺序
这里都使用var xxx = xxx
,在真正运行的时候并不知道谁先运行,所以这里的使用emojis[Index]
将会产生问题
class EmojiMemoryGame{
var emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁"]
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
修改为static类型
class EmojiMemoryGame{
static var emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁"]
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
Lecture4
修改代码
// View
import SwiftUI
struct ContentView: View {
let viewModel: EmojiMemoryGame
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}.foregroundColor(.pink)
}
.padding(.horizontal)
}
}
struct CardView: View{
let card: MemoryGame<String>.Card
var body: some View{
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth:10)
Text(card.content).font(.largeTitle)
}else{
shape.fill()
}
}
}
}
// ViewModel
// ViewModel制定了String类
import SwiftUI
class EmojiMemoryGame{
static let emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁","⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🎱","🛼","🥊","🍎","🍇","🍐","🍌","🍋","🍊","🍉","🍓","🫐","🍒","🍈","🍑","🥦","🍍","🥥","🥝","🍆","🥑"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 3) { pairIndex in
emojis[pairIndex]
}
}
private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
}
// Model
// Model中提供一个Card类,但不指定类型
import Foundation
struct MemoryGame<CardContent> {
private(set) var cards: Array<Card>
func choose(_ card: Card){
print("hello")
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
}
struct Card: Identifiable{
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
var id: Int
}
}
//main
//主程序创建一个ViewModel和一个View
import SwiftUI
@main
struct Memorize2App: App {
let game = EmojiMemoryGame()
var body: some Scene {
WindowGroup {
ContentView(viewModel: game)
}
}
}
View界面预览代码修改
因为ContentView的修改,需要提供ViewModel,故在预览界面也需要更改
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
ContentView(viewModel: game)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.previewInterfaceOrientation(.landscapeRight)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.preferredColorScheme(.dark)
.previewDevice("iPhone 11 Pro")
}
}
构建View-ViewMode点击事件
// View
.onTapGesture {
viewModel.choose(card)
}
// ViewModel
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
func choose(_ card: Card){
print("hello")
}
让bool值反转
card.isFaceUp.toggle()
internal external name
let chosenIndex = index(of: card)
//first is external name
//second is internal name
//third is struct
func index(of card: Card) -> Int {
for index in 0..<cards.count {
if cards[index].id == card.id {
return index
}
}
return 0
}
print(“( )”)
print("chosenCard = \(chosenCard)")
struct Card: Identifiable{
var isFaceUp: Bool = true
var isMatched: Bool = false
var content: CardContent
var id: Int
}
print会将可能的一切转换为String打印出来,只要加上\( )
struct 复制=
struct在复制的时候是完整的复制,既复制后新的与原来的无关了就
struct本身不可更改
我们加上mutating
//this function can change this struct
mutating func choose(_ card: Card) {
let chosenIndex = index(of: card)
cards[chosenIndex].isFaceUp.toggle()
print("\(cards)")
}
此时数据已经发生改变了,但UI不会变化
让ViewModel能够通知View更改
类加上ObservableObject,同时在需要通知的地方加上objectWillChange.send()
import SwiftUI
class EmojiMemoryGame: ObservableObject{
static let emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁","⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🥝","🍆","🥑"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
objectWillChange.send()
model.choose(card)
}
}
或者是将,需要更改后就要通知的变量加上@Published
import SwiftUI
class EmojiMemoryGame: ObservableObject{
static let emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁","⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🎱","🛼","🥊","🍎","🍇","🍐","🍌","🍋","🍊","🍉","🍓","🫐","🍒","🍈","🍑","🥦","🍍","🥥","🥝","🍆","🥑"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
}
此时让View观察着ViewModel,增添上@ObservedObject
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}.foregroundColor(.pink)
}
.padding(.horizontal)
}
}
MVVM下的成品代码
//
// Memorize2App.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
//
import SwiftUI
@main
struct Memorize2App: App {
let game = EmojiMemoryGame()
var body: some Scene {
WindowGroup {
ContentView(viewModel: game)
}
}
}
//
// ContentView.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// View
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}.foregroundColor(.pink)
}
.padding(.horizontal)
}
}
struct CardView: View{
let card: MemoryGame<String>.Card
var body: some View{
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth:10)
Text(card.content).font(.largeTitle)
}else{
shape.fill()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
ContentView(viewModel: game)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.previewInterfaceOrientation(.landscapeRight)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.preferredColorScheme(.dark)
.previewDevice("iPhone 11 Pro")
}
}
//
// EmojiMemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// ViewModel
import SwiftUI
class EmojiMemoryGame: ObservableObject{
static let emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁","⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🎱","🛼","🥊","🍎","🍇","🍐","🍌","🍋","🍊","🍉","🍓","🫐","🍒","🍈","🍑","🥦","🍍","🥥","🥝","🍆","🥑"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
}
//
// MemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// Model
import Foundation
struct MemoryGame<CardContent> {
private(set) var cards: Array<Card>
mutating func choose(_ card: Card) {
let chosenIndex = index(of: card)
cards[chosenIndex].isFaceUp.toggle()
}
func index(of card: Card) -> Int {
for index in 0..<cards.count {
if cards[index].id == card.id {
return index
}
}
return 0
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
}
struct Card: Identifiable{
var isFaceUp: Bool = true
var isMatched: Bool = false
var content: CardContent
var id: Int
}
}
enum
like a struct
a value type, it is copied as it is passed around
enum FastFoodMenuItem{
case hambuger //state
case fries
case drink
case cookie
}
associated data
each state can have its own “associated data”
enum FastFoodMenuItem{
case hambuger(numberOfPatties: Int)
case fries(size: FryOrderSize)
case drink(String, ounces: Int)
case cookie
}
// so FryOrderSize would be an enum too
enum FryOderSize{
case large
case small
}
set the value
let menuItem: FastFoodMenuItem = FastFoodMenuItem.hamburger(patties: 2)
var otherItem: FastFoodMenuItem = FastFoodMenuItem.cookie
//或者简写
let menuItem = FastFoodMenuItem.hamburger(patties: 2)
var otherItem: FastFoodMenuItem = .cookie
check an enum’s state
var menuItem = FastFoodMenuItem.hambuger(patties: 2)
switch menuItem{
case FastFoodMenuItem.hamburger: print("burger")
case FastFoodMenuItem.fries: print("fries")
case FastFoodMenuItem.drink: print("drink")
case FastFoodMenuItem.cookie: print("cookie")
}
// 简化为
switch menuItem{
case .hamburger: print("burger")
case .fries: print("fries")
case .drink: break
case .cookie: print("cookie")
default: print("other")
}
Associated data is accessed through a switch statement using this let syntax
var menuItem = FastFoodMenuItem.drink("Coke", ounces: 32)
switch menuItem {
case hamburger(let pattyCount): print("a burger with \(pattyCount) patties!")
case .fries(let size): print("a \(size) order of fries")
case .drink(let brand, let ounces): print("a \(ounces)oz \(brand)")
case .cookie: print("a cookie!")
}
methods and properties on an enum
an enum can have methods(and computed properties) but no stored properties
Notice: the use of _
if we don’t care about that piece of associated data
enum FastFoodMenuItem {
case hamburger(numberOfPatties: Int)
case fries(size: FryOrderSize)
case drink(String, ounces: Int)
case cookie
func isInludedInSpecialOrder(number: Int) -> Bool {
switch self {
case .hamburger(let pattyCount): return pattyCount == number
case .fries, .cookie: return true// a drink and cookie in every special order
case .drink(_, let ounces): return ounces == 16//&16oz drink of any kind
}
}
var calories: Int{ }
}
get all the cases
enum TeslaModel: CaseIterable {
case X
case S
case Three
case Y
}
//Now this enum will have a static var allCases that you can iterate over
for model in TeslaModel.allCases {
reportSaledNumbers(for: model)
}
func reportSalesNumbers(for model:TeslaModel) {
switch model {...}
}
switch
let s: String = "hello"
switch s {
case "goodbye": ...
case "hello": ...
default: ...
}
multiple lines allowed
var menuItem = FastFoodMenuItem.fries(sizes: FryOrderSize.large)
switch menuItem{
case .hamburger: print("burger")
case .fries:
print("yummy")
print("fries")
case .drink:
print("drink")
case .cookie: print("cookie")
}
Optional
It’s just an enum
enum Optional<T>{
case none
case some(T)
}
we have a value that can sometimes be “not set” or “unspecified” or “undertermined”
nil
var hello: String? var hello: Optional<String> = .none
var hello: String? = "hello" var hello: Optional<String> = .some("hello")
var hello: String? = nil. var hello: Optional<String> = .none
You can then assign it the value nil (Optional.none)
Or you can assign it something of the type T
can access the associated value either by force(with !)
enum Optional<T> {
case none
case some(T)
}
let hello: String? = ...
print(hello!)
switch hello {
case .none: //raise an exception(crash)
case .some(let data): print(data)
}
if let safely-gotten associated value
if let safehello = hello {
print(safehello)
} else {
//do something else
}
// 就是下面这段程序的含义
switch hello {
case .none: { //do something else }
case .some(let data): print(data)
}
??
enum Optional<T> {
case none
case some(T)
}
let x: String? = ...
let y = x ?? "foo"
// 就是下面这段程序的含义
switch x {
case .none: y = "foo"
case .some(let data): y = data
}
Optional chaining
enum Optional<T> {
case none
case some(T)
}
let x: String? = ...
let y = x?.foo()?.bar?.z
switch x {
case .none: y = nil
case .some(let xval)):
switch xval.foo() {
case .none: y = nil
case .some(let xfooval):
switch xfooval.bar {
case .none: y = nil
case .some(let xfbval): y = xfbval.z
}
}
}
简化firstIndex
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: {$0.id == card.id}) {
cards[chosenIndex].isFaceUp.toggle()
}
}
if let and分隔符,
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}) , !cards[chosenIndex].isFaceUp{ //如果不是nil才执行
isMatched的卡片处理
课程最终代码
// Memorize2App.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// Main
import SwiftUI
@main
struct Memorize2App: App {
let game = EmojiMemoryGame()
var body: some Scene {
WindowGroup {
ContentView(viewModel: game)
}
}
}
// ContentView.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// View
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}.foregroundColor(.pink)
}
.padding(.horizontal)
}
}
struct CardView: View{
let card: MemoryGame<String>.Card
var body: some View{
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth:10)
Text(card.content).font(.largeTitle)
} else if card.isMatched{
shape.opacity(0) // 不透明度设置为0,相当于完全透明
} else {
shape.fill()
}
}
}
}
// EmojiMemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// ViewModel
import SwiftUI
class EmojiMemoryGame: ObservableObject{
static let emojis = ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁","⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🎱","🛼","🥊"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 8) { pairIndex in
emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
}
// MemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// Model
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable{
private(set) var cards: Array<Card>
private var indexOfTheOneAndOnlyFaceUpCard: Int?
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{ //如果不是nil才执行
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard { //如果indexOfTheOneAndOnlyFaceUpCard为nil则去下面的else
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else { //此处指之前已经选了两个卡值已经被设置为nil,或一张卡都没选
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
//执行完逻辑后在把选的卡翻面
cards[chosenIndex].isFaceUp.toggle()
}
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
}
struct Card: Identifiable{
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
var id: Int
}
}
Lecture 4 Assignment II
Memorize
Required Tasks
1.Get the Memorize game working as demonstrated in lectures 1 through 4. Type in all the code. Do not copy/paste from anywhere.
2.If you’re starting with your assignment 1 code, remove your theme-choosing buttons and (optionally) the title of your game.
3.Add the formal concept of a “Theme” to your Model. A Theme consists of a name for the theme, a set of emoji to use, a number of pairs of cards to show, and an appropriate color to use to draw the cards.
4.At least one Theme in your game should show fewer pairs of cards than the number of emoji available in that theme.
5.If the number of pairs of emoji to show in a Theme is fewer than the number of emojis that are available in that theme, then it should not just always use the first few emoji in the theme. It must use any of the emoji in the theme. In other words, do not have any “dead emoji” in your code that can never appear in a game.
6.Never allow more than one pair of cards in a game to have the same emoji on it.
7.If a Theme mistakenly specifies to show more pairs of cards than there are emoji available, then automatically reduce the count of cards to show to match the count of available emoji.
8.Support at least 6 different themes in your game.
9.A new theme should be able to be added to your game with a single line of code.
10.Add a “New Game” button to your UI (anywhere you think is best) which begins a brand new game.
11.A new game should use a randomly chosen theme and touching the New Game button should repeatedly keep choosing a new random theme.
12.The cards in a new game should all start face down.
13.The cards in a new game should be fully shuffled. This means that they are not in any predictable order, that they are selected from any of the emojis in the theme (i.e. Required Task 5), and also that the matching pairs are not all side-by-side like they were in lecture (though they can accidentally still appear side-by-side at random).
14.Show the theme’s name in your UI. You can do this in whatever way you think looks best.
15.Keep score in your game by penalizing 1 point for every previously seen card that is involved in a mismatch and giving 2 points for every match (whether or not the cards involved have been “previously seen”). See Hints below for a more detailed explanation. The score is allowed to be negative if the user is bad at Memorize.
16.Display the score in your UI. You can do this in whatever way you think looks best.
Hints
1.Economy is still (and is always) valuable in coding.
2. Your ViewModel’s connection to its Model can consist of more than a single var model. It can be any number of vars. The “Model” is a conceptual entity, not a single struct.
3.A Theme is a completely separate thing froma MemoryGame(even though both are part of your application’s Model). You should not need to modify a single line of code in MemoryGame.swift to support themes!
4.Since a Theme is now part of your Model, it must be UI-independent. Representing a color in a UI-independent way is surprisingly nuanced (not just in Swift, but in general). We recommend, therefore, that you represent a color in your Theme as a simple String which is the name of the color, e.g. “orange”. Then let your ViewModel do one of its most important jobs which is to “interpret” the Model for the View. It can provide the View access to the current theme’s color in a UI-dependent representation (like SwiftUI’s Color struct
, for example).
5.You don’t have to support every named color in the world (a dozen or so is fine), but be sure to do something sensible if your Model contains a color (e.g. “fuchsia”) that the ViewModel does not know how to interpret.
6.We’ll learn a better (though still not perfect) way to represent a color in a UI independent fashion later in the quarter.
7.Required Task 6 means that, for example, a Halloween game should never have four 🎃 cards.
8.Required Task 7 means that, for example, if a theme’s emojis are [“👻”, “🎃”, “🕷”]
and the number of pairs to show in the theme is 47, you must automatically reduce that 47 to 3.
9.You might find Array’s randomElement()
function useful in this assignment (though note that this function (understandably) returns an Optional, so be prepared for that!). This is just a Hint, not a Required Task.
10.There is no requirement to use an Optional in this assignment (though you are welcome to do so if you think it would be part of a good solution).
11.You’ll very likely want to keep the static func createMemoryGame()
from lecture to create a new memory game. But that function needs a little bit more information to do its job now, so you will almost certainly have to add an argument to it.
12.On the other hand, you obviously won’t need the static let emojis
from last week’s lecture anymore because emojis are now obtained from whatever the current Theme is.
13.It’s quite likely that you will need to add an init() to your ViewModel. That’s because you’ll probably have one var whose initialization depends on another var. You can resolve this kind of catch-22 in an init() because, in an init(), you can control the order in which vars get initialized (whereas, when you use property initializers to initialize vars, the order is undetermined, which is why property initializers are not allowed to reference other vars).
14.The code in your ViewModel’s init() might look very, very similar to the code involved with your new game mechanism since you obviously want to start a new game in both of these places. Don’t worry if you end up with some code duplication here (you probably don’t quite know enough Swift yet to factor this code out).
15.You might well have to shuffle two different Arrays in this assignment. This is just a Hint, not a Required Task.
16.An amazing thing about “in-line functions” (actually called “closures”) in Swift is that if you declare a local variable in the scope that contains a closure, the closure can use that variable! For example, if foo below is a function that takes a function () -> Void as an argument, then …
let greetings = [“Hello”,“Howdy”,“Heya”].shuffled()
foo {
print(greetings) // this is legal! greetings is usable here!
}
This might come in handy for Required Task 5.
17.Make sure you think carefully about where all of your code lives (i.e. is it in the View, or in the ViewModel, or in the Model?). This assignment is mostly about MVVM, so this is very important to get right.
18.We’re not making this a Required Task (yet), but try to put the keyword private or private(set) on any variables where you think it would be appropriate.
19.A card has “already been seen” only if it has, at some point, been face up and then is turned back face down. So tracking “seen” cards is probably something you’ll want to do when you turn a card that is face up to be face down.
20.If you flipped over a 🐧 + 👻 , then flipped over a ✏ + 🏀 , then flipped over two 👻 s, your score would be 2 because you’d have scored a match (and no penalty would be incurred for the flips involving 🐧 , ✏ or 🏀 because they have not (yet) been involved in a mismatch, nor was the 👻 ever involved in a mismatch). If you then flipped over the 🐧 again + 🐼 , then flipped 🏀 + 🐧 once more, your score would drop 3 full points down to -1 overall because that 🐧 card had already been seen (on the very first flip) and subsequently was involved in two separate mismatches (scoring -1 for each mismatch) and the 🏀 was mismatched after already having been seen (-1). If you then flip 🐧 + the other 🐧 that you finally found, you’d get 2 points for a match and be back up to 1 total point.
21.The “already been seen” concept is about specific cards that have already been seen, not emoji that have been seen.
My Code
// ContentView.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// View
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
VStack{
Text("Theme: " + EmojiMemoryGame.ThemeName).font(.largeTitle)
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}.foregroundColor(EmojiMemoryGame.ThemeColor)
HStack{
VStack{
Text("Scores:").foregroundColor(.red)
Text(EmojiMemoryGame.scores.formatted())
}
Spacer()
Button{
viewModel.changeTheme()
} label:{
Text("Change Theme")
}
}
}
.padding(.horizontal)
}
}
struct CardView: View{
let card: MemoryGame<String>.Card
var body: some View{
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth:10)
Text(card.content).font(.largeTitle)
} else if card.isMatched{
shape.opacity(0) // 不透明度设置为0,相当于完全透明
} else {
shape.fill()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
ContentView(viewModel: game)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.previewInterfaceOrientation(.landscapeRight)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.preferredColorScheme(.dark)
.previewDevice("iPhone 11 Pro")
}
}
// EmojiMemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// ViewModel
import SwiftUI
class EmojiMemoryGame: ObservableObject{
static var cardsNumMin = 5
static var ThemeColor = Color.white
static var ThemeName = "None"
static var ThemeNum = 0
static var Themes = [
Theme(name: "Vehicles", emojis: ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁"], color: .gray),
Theme(name: "Balls", emojis: ["⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🎱"], color: .blue),
Theme(name: "Friuts", emojis: ["🍎","🍇","🍐","🍌","🍋","🍊","🍉","🍓","🫐","🍒","🍈","🍑"], color: .orange),
Theme(name: "Vegetables", emojis: ["🥦","🍍","🥬","🥒","🍆","🥑","🌶️","🥕"], color: .green),
Theme(name: "Faces", emojis: ["😂","😀"], color: .yellow)]
static var scores = 0
static func createMemoryGame() -> MemoryGame<String> {
ThemeNum = Int.random(in: 0..<Themes.count)
Themes[ThemeNum].emojis.shuffle()
ThemeColor = Themes[ThemeNum].color
ThemeName = Themes[ThemeNum].name
let themeCount = chooseCardsNum(Themes[ThemeNum].emojis.count)
return MemoryGame<String>(numberOfPairsOfCards: themeCount){ pairIndex in
Themes[ThemeNum].emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
static func chooseCardsNum(_ emojisMaxNum: Int)-> Int{
if emojisMaxNum > 3 {
return Int.random(in: 3..<emojisMaxNum)
} else {
return emojisMaxNum
}
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
func changeTheme() {
var newThemeNum = Int.random(in: 0..<EmojiMemoryGame.Themes.count)
while(newThemeNum == EmojiMemoryGame.ThemeNum) { //解决change之后有可能会随机到上一次一样的问题
newThemeNum = Int.random(in: 0..<EmojiMemoryGame.Themes.count)
}
EmojiMemoryGame.Themes[newThemeNum].emojis.shuffle()
EmojiMemoryGame.ThemeColor = EmojiMemoryGame.Themes[newThemeNum].color
EmojiMemoryGame.ThemeName = EmojiMemoryGame.Themes[newThemeNum].name
EmojiMemoryGame.ThemeNum = newThemeNum
let newThemeCount = EmojiMemoryGame.chooseCardsNum(EmojiMemoryGame.Themes[newThemeNum].emojis.count)
model.changeTheme(numberOfPairsOfCards: newThemeCount){ pairIndex in
EmojiMemoryGame.Themes[newThemeNum].emojis[pairIndex]
}
EmojiMemoryGame.scores = 0
}
}
// MemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// Model
import Foundation
import SwiftUI
struct MemoryGame<CardContent> where CardContent: Equatable{
private(set) var cards: Array<Card>
private var indexOfTheOneAndOnlyFaceUpCard: Int?
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
cards.shuffle()
}
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{ //如果不是nil才执行
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard { //如果indexOfTheOneAndOnlyFaceUpCard为nil则去下面的else
if cards[chosenIndex].content == cards[potentialMatchIndex].content { //
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
EmojiMemoryGame.scores += 2
}
else {
if cards[chosenIndex].isBeenSeen == true {
EmojiMemoryGame.scores -= 1
} else {
cards[chosenIndex].isBeenSeen = true
}
if cards[potentialMatchIndex].isBeenSeen == true {
EmojiMemoryGame.scores -= 1
} else {
cards[potentialMatchIndex].isBeenSeen = true
}
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else { //此处指之前已经选了两个卡值已经被设置为nil,或一张卡都没选
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
//执行完逻辑后在把选的卡翻面
cards[chosenIndex].isFaceUp.toggle()
}
}
mutating func changeTheme(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
cards.shuffle()
}
struct Card: Identifiable{
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
var id: Int
var isBeenSeen: Bool = false
}
}
struct Theme{
var name: String
var emojis: Array<String>
var color: Color
}
Extra Credit
We try to make Extra Credit be opportunities to expand on what you’ve learned this week. Attempting at least some of these each week is highly recommended to get the most out of this course.
If you choose to tackle an Extra Credit item, mark it in your code with comments so your grader can find it.
1.When your code creates a Theme, allow it to default to use all the emoji available in the theme if the code that creates the Theme doesn’t want to explicitly specify how many pairs to use. This will require adding an init or two to your Theme struct.
2.Allow the creation of some Themes where the number of pairs of cards to show is not a specific number but is, instead, a random number. We’re not saying that every Theme now shows a random number of cards, just that some Themes can now be created to show a random number of cards (while others still are created to show a specific, pre-determined number of cards).
3.Supportagradientasthe“color”foratheme.Hint:fill()cantakeaGradientasits argument rather than a Color. This is a “learning to look things up in the documentation” exercise.
4.Modify the scoring system to give more points for choosing cards more quickly. For example, maybe you get max(10 - (number of seconds since last card was chosen), 1) x (the number of points you would have otherwise earned or been penalized with). (This is just an example, be creative!). You will definitely want to familiarize yourself with the Date struct.
My Extra Credit Code
每次配对成功时获得的score为max(上一次配对时间, 1)
每次配对失败时扣除3分
// MemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// Model
import Foundation
import SwiftUI
struct MemoryGame<CardContent> where CardContent: Equatable{
private(set) var cards: Array<Card>
private var indexOfTheOneAndOnlyFaceUpCard: Int?
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
cards.shuffle()
}
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id}),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{ //如果不是nil才执行
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard { //如果indexOfTheOneAndOnlyFaceUpCard为nil则去下面的else
if cards[chosenIndex].content == cards[potentialMatchIndex].content { //匹配成功
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
EmojiMemoryGame.scores += max(10 - abs(Int(EmojiMemoryGame.timeval.timeIntervalSinceNow)), 1)
EmojiMemoryGame.matchedNum += 1
if EmojiMemoryGame.matchedNum == EmojiMemoryGame.Themes[EmojiMemoryGame.ThemeNum].emojisNum { //匹配成功
cards[chosenIndex].isFaceUp.toggle()
cards[indexOfTheOneAndOnlyFaceUpCard!].isFaceUp.toggle()
}
EmojiMemoryGame.timeval = Date()
}
else {
if cards[chosenIndex].isBeenSeen == true {
EmojiMemoryGame.scores -= 3
} else {
cards[chosenIndex].isBeenSeen = true
}
if cards[potentialMatchIndex].isBeenSeen == true {
EmojiMemoryGame.scores -= 3
} else {
cards[potentialMatchIndex].isBeenSeen = true
}
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else { //此处指之前已经选了两个卡值已经被设置为nil,或一张卡都没选
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
//执行完逻辑后在把选的卡翻面
cards[chosenIndex].isFaceUp.toggle()
}
}
mutating func changeTheme(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content,id: pairIndex * 2))
cards.append(Card(content: content,id: pairIndex * 2 + 1))
}
cards.shuffle()
indexOfTheOneAndOnlyFaceUpCard = nil
}
struct Card: Identifiable{
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
var id: Int
var isBeenSeen: Bool = false
}
}
struct Theme{
var name: String
var emojis: Array<String>
var color: Color
var emojisNum: Int = 0
init(name: String, emojis: Array<String>, color: Color) {
self.name = name
self.emojis = emojis
self.color = color
}
mutating func newEmojiNum() {
if emojis.count < 3 {
emojisNum = emojis.count
} else {
emojisNum = Int.random(in: 3..<emojis.count)
}
}
}
// EmojiMemoryGame.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// ViewModel
import SwiftUI
class EmojiMemoryGame: ObservableObject{
static var cardsNumMin = 5
static var ThemeColor = Color.white
static var ThemeName = "None"
static var ThemeNum = 0
static var Themes = [
Theme(name: "Vehicles", emojis: ["🛺","🚑","🚎","🚃","🚜","🛩️","🚀","🚁"], color: .gray),
Theme(name: "Balls", emojis: ["⚽️","🏀","🏈","⚾️","🥎","🎾","🏉","🎱"], color: .blue),
Theme(name: "Friuts", emojis: ["🍎","🍇","🍐","🍌","🍋","🍊","🍉","🍓","🫐","🍒","🍈","🍑"], color: .orange),
Theme(name: "Vegetables", emojis: ["🥦","🍍","🥬","🥒","🍆","🥑","🌶️","🥕"], color: .green),
Theme(name: "Faces", emojis: ["😂","😀"], color: .yellow),
Theme(name: "None", emojis: ["A"], color: .cyan)]
static var scores = 0
static var matchedNum = 0
static var timeval = Date()
static var timeLeft = ""
static func createMemoryGame() -> MemoryGame<String> {
ThemeNum = Int.random(in: 0..<Themes.count)
Themes[ThemeNum].emojis.shuffle()
ThemeColor = Themes[ThemeNum].color
ThemeName = Themes[ThemeNum].name
Themes[ThemeNum].newEmojiNum()
timeval = Date()
return MemoryGame<String>(numberOfPairsOfCards: Themes[ThemeNum].emojisNum){ pairIndex in
Themes[ThemeNum].emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
//MARK: - Intent(s)
func choose(_ card:MemoryGame<String>.Card){
model.choose(card)
}
func changeTheme() {
var newThemeNum = Int.random(in: 0..<EmojiMemoryGame.Themes.count)
while(newThemeNum == EmojiMemoryGame.ThemeNum) { //解决change之后有可能会随机到上一次一样的问题
newThemeNum = Int.random(in: 0..<EmojiMemoryGame.Themes.count)
}
EmojiMemoryGame.Themes[newThemeNum].emojis.shuffle()
EmojiMemoryGame.ThemeColor = EmojiMemoryGame.Themes[newThemeNum].color
EmojiMemoryGame.ThemeName = EmojiMemoryGame.Themes[newThemeNum].name
EmojiMemoryGame.ThemeNum = newThemeNum
EmojiMemoryGame.Themes[EmojiMemoryGame.ThemeNum].newEmojiNum()
model.changeTheme(numberOfPairsOfCards: EmojiMemoryGame.Themes[EmojiMemoryGame.ThemeNum].emojisNum){ pairIndex in
EmojiMemoryGame.Themes[newThemeNum].emojis[pairIndex]
}
EmojiMemoryGame.scores = 0
EmojiMemoryGame.timeval = Date()
EmojiMemoryGame.matchedNum = 0
}
}
//
// ContentView.swift
// Memorize2
//
// Created by zhj12399 on 2023/1/6.
// View
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
VStack{
Text("Theme: " + EmojiMemoryGame.ThemeName).font(.largeTitle)
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}.foregroundColor(EmojiMemoryGame.ThemeColor)
HStack{
VStack{
Text("Scores:").foregroundColor(.red)
Text(EmojiMemoryGame.scores.formatted())
}
Spacer()
Button{
viewModel.changeTheme()
} label:{
Text("Change Theme")
}
}
}
.padding(.horizontal)
}
}
struct CardView: View{
let card: MemoryGame<String>.Card
var body: some View{
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp{
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth:10)
Text(card.content).font(.largeTitle)
} else if card.isMatched{
shape.opacity(0) // 不透明度设置为0,相当于完全透明
} else {
shape.fill()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
ContentView(viewModel: game)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.previewInterfaceOrientation(.landscapeRight)
.preferredColorScheme(.light)
.previewDevice("iPhone 11 Pro")
ContentView(viewModel: game)
.preferredColorScheme(.dark)
.previewDevice("iPhone 11 Pro")
}
}
Lecture 3-4 课程资源
Lecture 3-4 Reading
Lecture 3-4 Assignment
Lecture 3-4 Homework