• Laying Out UI
  • StackView
  • mapping array

ViewController.swift

override func viewDidLoad() {
        super.viewDidLoad()
        
        let redView = UIView()
        redView.backgroundColor = .red
        let blueView = UIView()
        blueView.backgroundColor = .blue
        
        let stackView = UIStackView(arrangedSubviews: [redView, blueView])
        stackView.distribution = .fillEqually
        stackView.axis = .vertical
        
        view.addSubview(stackView)
        stackView.frame = .init(x: 0, y: 0, width: 300, height: 200)
    }

stackView의 distribution을 설정하지 않으면 하나의 view만 나온다.

stackView.frame = .initwi로 검색하여 frame을 설정할 수 있다.

stackView.axis = .vertical

// this enables auto layout for us
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
		let redView = UIView()
        redView.backgroundColor = .red
        redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
        let blueView = UIView()
        blueView.backgroundColor = .blue
        let yellowView = UIView()
        yellowView.backgroundColor = .yellow
        yellowView.heightAnchor.constraint(equalToConstant: 120).isActive = true

redView와 yellowView의 height를 특정한 값으로 변경해주면 warning이 발생 왜냐하면 stackView에서 .fillEqually로 해줬기 때문

.fillEqually를 제거해주면 red, yellow view의 height는 변한다.

  • 3 buttons on top
  • 5 buttons on bottom

Using StackView

map

map을 사용하여 간편하고 재밌게 배열 만들기

let subviews = [UIColor.gray, .darkGray, .black].map
{ (color) -> UIView in
    let v = UIView()
    v.backgroundColor = color
    return v
}

UIColor 배열을 각 UIColor를 backgroundColor로 가지는 UIView 배열로 mapping 가능

[UIColor.gray, .darkGray, .black].map(<#T##transform: (UIColor) throws -> T##(UIColor) throws -> T#>)

map 호출 시 기본형태는 다음과 같다.

[UIColor.gray, .darkGray, .black].map
            { (<#UIColor#>) -> T in
            <#code#>
        }

enter를 치면 괄호안에 내가 mapping할 타입을 보여준다. 인자명을 잡아주고 T는 제너릭 타입이므로 우리가 사용할 UIView로 바꿔준다.

그리고 code에서 제너릭 타입에 써준 값으로 return

topStackView

 		let subviews = [UIColor.gray, .darkGray, .black].map
        { (color) -> UIView in
            let v = UIView()
            v.backgroundColor = color
            return v
        }
        
        let topStackView = UIStackView(arrangedSubviews: subviews)
        topStackView.axis = .horizontal
        topStackView.distribution = .fillEqually
        topStackView.heightAnchor.constraint(equalToConstant: 100).isActive = true

그래서 mapping으로 만든 subviews를 topStackView에 넣어준다. (이전에 redView의 이름을 변경)

refactoring-rename : command + control + E

StackView의 기본 axis = .horizontal이다. 그래서 위 코드는 불필요하다.

####buttonsStackView

let bottomSubviews = [UIColor.red, .yellow, .purple, .cyan, .orange].map
        { (color) -> UIView in
            let v = UIView()
            v.backgroundColor = color
            return v
        }
        
        let buttonsStackView = UIStackView(arrangedSubviews: bottomSubviews)
        buttonsStackView.distribution = .fillEqually
        buttonsStackView.heightAnchor.constraint(equalToConstant: 120).isActive = true

HomeBottomControlsStackView

import UIKit

class HomeBottomControlsStackView: UIStackView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        let bottomSubviews = [UIColor.red, .yellow, .purple, .cyan, .orange].map
        { (color) -> UIView in
            let v = UIView()
            v.backgroundColor = color
            return v
        }
        
        // arrangedSubviews = bottomSubviews
        bottomSubviews.forEach { (v) in
            addArrangedSubview(v)
        }
        
        distribution = .fillEqually
        heightAnchor.constraint(equalToConstant: 120).isActive = true
    }
    
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }   
}

StackView로 만들기 위해서 Cocoa Touch Class로 만들어주고 StackView를 상속. 이 파일은 StackView이기 때문에 네이밍을 할 때 끝에 StackView를 붙여주자

ViewController에서 선언했던 buttonsStackView의 속성들 (distribution, height 등)을 이제 여기에서 깔끔하게 선언을 해준다.

// 사진넣기

StackView선언에서 사용했었던 arrangedSubviews는 readonly다.

bottomSubviews.forEach { (v) in
            addArrangedSubview(v)
        }

addArrangedSubview()와 forEach를 사용하여 StackView에 넣어준다.

let buttonsStackView = HomeBottomControlsStackView()

그러면 우리가 StackView인 HomeBottomControlsStackView를 ViewController에서 선언할 때 인자가 필요없다. 지워준다

Buttons

let subviews = [#imageLiteral(resourceName: "refresh_circle"), #imageLiteral(resourceName: "dismiss_circle"), #imageLiteral(resourceName: "super_like_circle"), #imageLiteral(resourceName: "like_circle"), #imageLiteral(resourceName: "boost_circle")].map { (img) -> UIView in
            let button = UIButton(type: .system)
            button.setImage(img.withRenderingMode(.alwaysOriginal), for: .normal)
            return button
        }
        
        subviews.forEach { (v) in
            addArrangedSubview(v)
        }

UIImage에 image를 가져올 때 imageLiteral(resourceName: <#T##String#>) 을 사용하면 더블클릭하여 이미지를 편하게 가져올 수 있다.

여기서 map을 이용하여 image들을 UIView로 반환시키는데 closure안에서는 UIButton을 반환시킨다.

관련 글 : https://leehonghwa.github.io/blog/responderChain/

UIButton은 UIView에 속해있기 때문

Button생성시 UIButton()의 인자로 type을 넣을 수 있는데 .system으로 하지 않으면 까맣게 어색하게 클릭된다.

// 이 부분은 공식문서를 참고해 더 공부해야한다.

image를 가져와서 사용할 때에는

button.setImage(<#T##image: UIImage?##UIImage?#>, for: <#T##UIControl.State#>)

withRenderingMode(.alwaysOriginal) 을 잊지말자 + for: .normal

overallStackView constraints

overallStackView.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, trailing: view.trailingAnchor)

기존 extension함수를 제거하고 safeAreaLayoutGuide에 넣는다.

여기서 인자에 leftAnchor, rightAnchor를 넣으면 오류가 뜬다.

// 사진넣기

TopNavigationStackView

BottomStackView와 동일하게 만들면 우리가 원하는 UI를 만들 수 없다.

	let settingsButton = UIButton(type: .system)
    let messageButton = UIButton(type: .system)
    let fireImageView = UIImageView(image: #imageLiteral(resourceName: "app_icon"))
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        fireImageView.contentMode = .scaleAspectFit
        
        settingsButton.setImage(#imageLiteral(resourceName: "top_left_profile").withRenderingMode(.alwaysOriginal), for: .normal)
        messageButton.setImage(#imageLiteral(resourceName: "top_right_messages").withRenderingMode(.alwaysOriginal), for: .normal)
        
        [settingsButton, UIView(), fireImageView, UIView(), messageButton].forEach { (v) in
            addArrangedSubview(v)
        }
        
        distribution = .equalCentering
        heightAnchor.constraint(equalToConstant: 80).isActive = true
    }

따라서 Nav에 사용될 두개의 버튼과 imageView를 init에서 만들지 않고 변수로 만들어준다. 그 뒤에 배열에 넣은 후 forEach문으로 넣어준다.

아래 쪽에 있는 Debug View hierarchy를 클릭하면 구체적으로 어떻게 레이아웃이 구성되어있는지 볼 수 있다. 각각의 요소들이 어느 크기를 가지고 있는지 확인하기 좋다.

그리고 여기서는 distribution을 .equalCentering으로 주었다. -> Document확인

		isLayoutMarginsRelativeArrangement = true
        layoutMargins = .init(top: 0, left: 16, bottom: 0, right: 16)

Margin을 주기위해 다음과 같이 코드를 작성

layoutMargins -> Document

Refactoring

import UIKit

class ViewController: UIViewController {

    let topStackView = TopNavigationStackView()
    let blueView = UIView()
    let buttonsStackView = HomeBottomControlsStackView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        blueView.backgroundColor = .blue
        setupLayout()
    }
    
    // MARK:- Fileprivate
    
    fileprivate func setupLayout() {
        let overallStackView = UIStackView(arrangedSubviews: [topStackView, blueView, buttonsStackView])
        overallStackView.axis = .vertical
        
        view.addSubview(overallStackView)
        overallStackView.frame = .init(x: 0, y: 0, width: 300, height: 200)
        
        overallStackView.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, trailing: view.trailingAnchor)
    }
}

viewDidLoad에서 선언해주었던 것들을 상위 block에서 선언한다.

그리고 overallStackView에 관한 것들은 Refactor > Extract Method를 사용해 메소드로 빼준다.

여기서 파일 타입은 fileprivate이고 주석에서 MARK:- Fileprivate 라고 해주면 위에서 찾기가 쉽다.

태그:

카테고리:

업데이트:

댓글남기기