www.youtube.com/watch?v=oDKDGCRdSHc
소스
GridLayout.swift
//
// GridLayout.swift
// Memorize
//
// Created by CS193p Instructor.
// Copyright © 2020 Stanford University. All rights reserved.
//
import SwiftUI
struct GridLayout {
private(set) var size: CGSize
private(set) var rowCount: Int = 0
private(set) var columnCount: Int = 0
init(itemCount: Int, nearAspectRatio desiredAspectRatio: Double = 1, in size: CGSize) {
self.size = size
// if our size is zero width or height or the itemCount is not > 0
// then we have no work to do (because our rowCount & columnCount will be zero)
guard size.width != 0, size.height != 0, itemCount > 0 else { return }
// find the bestLayout
// i.e., one which results in cells whose aspectRatio
// has the smallestVariance from desiredAspectRatio
// not necessarily most optimal code to do this, but easy to follow (hopefully)
var bestLayout: (rowCount: Int, columnCount: Int) = (1, itemCount)
var smallestVariance: Double?
let sizeAspectRatio = abs(Double(size.width/size.height))
for rows in 1...itemCount {
let columns = (itemCount / rows) + (itemCount % rows > 0 ? 1 : 0)
if (rows - 1) * columns < itemCount {
let itemAspectRatio = sizeAspectRatio * (Double(rows)/Double(columns))
let variance = abs(itemAspectRatio - desiredAspectRatio)
if smallestVariance == nil || variance < smallestVariance! {
smallestVariance = variance
bestLayout = (rowCount: rows, columnCount: columns)
}
}
}
rowCount = bestLayout.rowCount
columnCount = bestLayout.columnCount
}
var itemSize: CGSize {
if rowCount == 0 || columnCount == 0 {
return CGSize.zero
} else {
return CGSize(
width: size.width / CGFloat(columnCount),
height: size.height / CGFloat(rowCount)
)
}
}
func location(ofItemAt index: Int) -> CGPoint {
if rowCount == 0 || columnCount == 0 {
return CGPoint.zero
} else {
return CGPoint(
x: (CGFloat(index % columnCount) + 0.5) * itemSize.width,
y: (CGFloat(index / columnCount) + 0.5) * itemSize.height
)
}
}
}
Grid.swift
//
// Grid.swift
// Memorize
//
// Created by user on 2021/01/17.
//
import SwiftUI
struct Grid<Item, ItemView>: View where Item: Identifiable, ItemView: View{
private var items: [Item]
private var viewForItem: (Item) -> ItemView
init (_ items: [Item], viewForItem: @escaping (Item)-> ItemView) {
self.items = items
self.viewForItem = viewForItem
}
var body: some View {
GeometryReader { geometry in
body(for: GridLayout(itemCount: items.count, in: geometry.size))
}
}
private func body(for layout: GridLayout) -> some View {
ForEach(items) { item in
body(for: item, in: layout)
}
}
private func body(for item: Item, in layout: GridLayout) -> some View {
let index = items.firstIndex(matching: item)!
// return Group {
// if index != nil {
// viewForItem(item)
// .frame(width: layout.itemSize.width, height: layout.itemSize.height)
// .position(layout.location(ofItemAt: index!))
// }
// }
return viewForItem(item)
.frame(width: layout.itemSize.width, height: layout.itemSize.height)
.position(layout.location(ofItemAt: index))
}
}
Swift 3에서부터 매개변수로 사용되는 클로저는 @noescape가 기본값이 되었다. 따라서 탈출 클로저를 만들고 싶으면 매개변수 이후 콜론(:) 뒤에 @escaping라는 키워드를 붙여 클로저가 탈출하는 것을 허용할 수 있다.
탈출 클로저 (@escaping)
탈출불가 클로저와 달리, 함수가 종료된 뒤에 클로저가 필요한 경우를 생각해보자. 이 때는 함수가 종료되더라도 클로저는 사용될 때까지 메모리에 남아있어야 할 필요가 있다.
탈출 클로저를 사용할 수 있는 경우는 다음과 같다. 탈출 클로저가 필요한 상황에 @escaping를 표기하지 않으면 컴파일 에러가 발생한다.
- 클로저 저장: 글로벌 변수에 클로저를 저장하여 함수가 종료된 후에 사용되는 경우
주의할 점은 탈출 클로저 내부에서 해당 타입의 프로퍼티나 메서드 등에 접근하려면 반드시 self 키워드를 사용해야 한다는 것이다.
탈출 클로저의 라이프사이클은 다음과 같다.
- 함수의 전달인자로 클로저가 전달된다
- 함수 내부에서 몇 작업들을 수행한다
- 저장 또는 비동기 작업을 위해 클로저를 실행한다
- 함수 리턴
var complitionHandler: ((Int)->Void)?
func getSumOf(array:[Int], handler: @escaping ((Int)->Void)) {
//step 2
var sum: Int = 0
for value in array {
sum += value
}
//step 3
self.complitionHandler = handler
}
func doSomething() {
//step 1
self.getSumOf(array: [16,756,442,6,23]) { (sum) in
print(sum)
//step 4. 함수 종료
}
}
MemoryGame.swift
//
// MemoryGame.swift
// Memorize
//
// Created by user on 2021/01/16.
//
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable {
private(set) var cards: Array<Card>
private var indexOfTheOneAndOnlyFaceUpCard: Int? {
get {cards.indices.filter { cards[$0].isFaceUp }.only}
set {
for index in cards.indices {
cards[index].isFaceUp = index == newValue
}
}
}
mutating func choose (card: Card){
if let choseIndex = cards.firstIndex(matching: card), !cards[choseIndex].isFaceUp, !cards[choseIndex].isMatched {
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
if cards[choseIndex].content == cards[potentialMatchIndex].content {
cards[choseIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
cards[choseIndex].isFaceUp = true
}else {
indexOfTheOneAndOnlyFaceUpCard = choseIndex
}
}
}
init (numberOfPairsOfCards: Int, cardContentFactory: (Int)-> CardContent) {
cards = Array<Card>()
for pairIndex in 0 ..< numberOfPairsOfCards {
let content = cardContentFactory(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
}
}
EmojiMemoryGame.swift
//
// EmojiMemoryGame.swift
// Memorize
//
// Created by user on 2021/01/16.
//
import SwiftUI
class EmojiMemoryGame: ObservableObject {
@Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static func createMemoryGame() -> MemoryGame<String>{
let emojis: Array<String> = ["😊", "😱", "😎"]
return MemoryGame<String>(numberOfPairsOfCards: emojis.count) { parIndex in
return emojis[parIndex]
}
}
// MARK: - Access to the Model
var cards: Array<MemoryGame<String>.Card> {
model.cards
}
// MARK: - Intent(s)
func choose(card: MemoryGame<String>.Card){
model.choose(card: card)
}
}
EmojiMemoryGameView.swift
//
// ContentView.swift
// Memorize
//
// Created by user on 2021/01/15.
//
import SwiftUI
struct EmojiMemoryGameView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
Grid(viewModel.cards) { card in
CardView(card: card).onTapGesture {
viewModel.choose(card: card)
}
.padding(5)
}
.padding()
.foregroundColor(Color.orange)
}
}
struct CardView: View {
var card: MemoryGame<String>.Card
var body: some View {
GeometryReader(content: { geometry in
body(for: geometry.size)
})
}
@ViewBuilder
private func body(for size: CGSize) -> some View {
if card.isFaceUp || !card.isMatched {
ZStack {
Pie(startAngle: Angle.degrees(0-90), endAngle: Angle.degrees(110-90), clockwise: true)
.padding(5)
.opacity(0.4)
Text(card.content)
.font(Font.system(size: fontSize(for: size)))
}
// .modifier(Cardify(isFaceUp: card.isFaceUp))
.cardify(isFaceUp: card.isFaceUp)
}
}
// MARK: - Drawing Constants
private func fontSize(for size: CGSize) -> CGFloat {
min(size.width, size.height) * 0.7
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
game.choose(card: game.cards[0])
return EmojiMemoryGameView(viewModel: game)
}
}
developer.apple.com/documentation/swiftui/viewbuilder
Apple Developer Documentation
developer.apple.com
@ViewBuider를 이용하면, 아래의 것은 여러개의 뷰로, 리스트처럼 표현된다.
if 조건을 주었을 때만 실행
커스텀 가능
import SwiftUI
struct CustomVStack<Content: View>: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0) {
content()
}
}
}
struct CustomVStack_Previews: PreviewProvider {
static var previews: some View {
CustomVStack {
Text("1")
Text("2")
Text("3")
}
}
}
Array+Identifiable.swift
//
// Array+Identifiable.swift
// Memorize
//
// Created by user on 2021/01/17.
//
import Foundation
extension Array where Element: Identifiable {
func firstIndex(matching: Element) -> Int? {
for index in 0..<self.count {
if self[index].id == matching.id {
return index
}
}
return nil
}
}
Array+Only.swift
//
// Array_Only.swift
// Memorize
//
// Created by user on 2021/01/18.
//
import Foundation
extension Array {
var only: Element? {
count == 1 ? first: nil
}
}
Pie.swift
//
// Pie.swift
// Memorize
//
// Created by user on 2021/01/18.
//
import SwiftUI
struct Pie: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool = false
func path (in rect: CGRect) -> Path {
let center = CGPoint(x: rect.maxX / 2, y: rect.maxY / 2)
let radius = min(rect.width, rect.height) / 2
let start = CGPoint (
x: center.x + radius * cos(CGFloat(startAngle.radians)),
y: center.y + radius * sin(CGFloat(startAngle.radians))
)
var p = Path()
p.move(to: center)
p.addLine(to: start)
p.addArc (center: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: clockwise)
p.addLine(to: center)
return p
}
}
extension View {
func cardify (isFaceUp: Bool) -> some View {
modifier(Cardify(isFaceUp: isFaceUp))
}
}
Cardify.swift
//
// Cardify.swift
// Memorize
//
// Created by user on 2021/01/18.
//
import SwiftUI
struct Cardify: ViewModifier {
var isFaceUp: Bool
func body(content: Content) -> some View {
ZStack {
if isFaceUp {
RoundedRectangle(cornerRadius: conrnerRadius)
.fill(Color.white)
RoundedRectangle(cornerRadius: conrnerRadius)
.stroke(lineWidth: edgeLineWidth)
content
} else {
RoundedRectangle(cornerRadius: conrnerRadius)
.fill()
}
}
}
// MARK: - Drawing Constants
private let conrnerRadius: CGFloat = 10
private let edgeLineWidth: CGFloat = 3
}
ViewModifier란?
-
SwiftUI 프레임워크에 내장되어있는 한 프로토콜이고,
-
기존의 뷰 또는 다른 view modifier에 적용시켜 또 다른 버전을 만들 수 있는 modifier이다.
-
기존에 생성한 뷰 또는 modifier에 추가적으로 꾸며줄 수 있는 것이다.
developer.apple.com/documentation/swiftui/viewmodifier
Apple Developer Documentation
developer.apple.com
'IOS' 카테고리의 다른 글
3. IOS 강의 Multithreading EmojiArt (0) | 2021.01.18 |
---|---|
3. IOS 강의 Animation (0) | 2021.01.18 |
3. IOS 강의 Grid enum Optionals (0) | 2021.01.18 |
3. IOS 강의 Reactive UI + Protocols + Layout (0) | 2021.01.17 |
3. IOS 강의 MVVM and the Swift Type System (0) | 2021.01.17 |