본문 바로가기

IOS

3. IOS 강의 ViewBuilder Shape ViewModifier

www.youtube.com/watch?v=oDKDGCRdSHc

 

Slides.pdf
0.84MB

 

 

소스 

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 키워드를 사용해야 한다는 것이다.

탈출 클로저의 라이프사이클은 다음과 같다.

  1. 함수의 전달인자로 클로저가 전달된다
  2. 함수 내부에서 몇 작업들을 수행한다
  3. 저장 또는 비동기 작업을 위해 클로저를 실행한다
  4. 함수 리턴
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