카테고리 없음

날씨앱만들기 (2) UI 구성 1

bluewiper 2024. 7. 11. 18:50

 

일단은 UI 구성이 잘 되었는지 확인이 필요하니까 임의의 컬러, 텍스트값을 넣어서 UI 구성을 해준다. 

더보기
import UIKit
import SnapKit

class ViewController: UIViewController {

    //현재 위치한 도시를 알려주는 라벨
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "서울특별시"
        label.textColor = .white
        label.font = .boldSystemFont(ofSize: 30)
        return label
    }()
    
    //기온을 나타내는 라벨
    private let tempLabel: UILabel = {
        let label = UILabel()
        label.text = "20도"
        label.textColor = .white
        label.font = .boldSystemFont(ofSize: 50)
        return label
    }()
    
    //최저기온을 나타내는 라벨
    private let tempMinLabel: UILabel = {
        let label = UILabel()
        label.text = "20도"
        label.textColor = .white
        label.font = .boldSystemFont(ofSize: 20)
        return label
    }()
    
    //최고기온 나타내는 라벨
    private let tempMaxLabel: UILabel = {
        let label = UILabel()
        label.text = "20도"
        label.textColor = .white
        label.font = .boldSystemFont(ofSize: 20)
        return label
    }()
    
    //최저기온과 최고기온을 담는 Horizontal Stack View
    private let tempStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.spacing = 20
        stackView.distribution = .fillEqually
        return stackView
    }()
    
    //날씨이미지를 담은 이미지뷰
    private let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.backgroundColor = .gray
        return imageView
    }()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureUI()

    }

    private func configureUI() {
        view.backgroundColor = .black
        
        //각 프로퍼티 뷰 생성
        [
            titleLabel,
            tempLabel,
            tempStackView,
            imageView
        ].forEach { view.addSubview($0) }
        
        //tempStackView에 들어갈 최저기온과 최고기온 뷰 추가
        [
            tempMinLabel,
            tempMaxLabel
        ].forEach { tempStackView.addArrangedSubview($0) }
            
        //각 프로퍼티 제약 조건
        titleLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalToSuperview().offset(120)
        }
        
        tempLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalTo(titleLabel.snp.bottom).offset(10)
        }
        
        tempStackView.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalTo(tempLabel.snp.bottom).offset(10)
        }
        
        imageView.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.width.height.equalTo(160)
            $0.top.equalTo(tempStackView.snp.bottom).offset(20)
        }
    }
}

여기까지 구성한 뒤에 서버에서 불러온 데이터로 현재 위치, 현재 기온, 최저 및 최고 기온에 대해 적용해줄 수 있는 로직을 짜야 한다. 

아래가 제네릭을 사용해서 url을 가져오는 가장 일반적인 메서드라고 한다. 

우리가 하나의 url만 쓰는게 아니니까 하나의 타입을 지정하는 게 아니고, 재사용을 위해 일반적인 메서드를 쓰는 것 같다. 

더보기
// 서버 데이터를 불러오는 메서드
    private func fetchData<T: Decodable>(url: URL, completion: @escaping (T?) -> Void) {
        let session = URLSession(configuration: .default)
        session.dataTask(with: URLRequest(url: url)) { data, response, error in
            guard let data, error == nil else {
                print("데이터 로드 실패")
                completion(nil)
                return
            }
            // http status code 성공 범위는 200번대
            let successRange = 200..<300
            //HTTPURLResponse 안에 http status code를 깔기 위해 타입 캐스팅을 해준다.
            if let response = response as? HTTPURLResponse, successRange.contains(response.statusCode) {
                // 받은 데이터가 JSON 형태일테니 Swift 형태로 디코딩해준다.
                guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
                    print("JSON 디코딩 실패")
                    completion(nil)
                    return
                }
                // guard let decodedData가 성공했을 경우
                completion(decodedData)
                //성공 범위 안에 들지 못한다면(200번대)
            } else {
                print("응답 오류")
                completion(nil)
            }
        }.resume() // resume해야 작동한다.
    }

제네릭과 이스케이핑..들어본 것 같은데 ㅠㅠ  

private func fetchData<T: Decodable>(url: URL, completion: @escaping (T?) -> Void) {
        let session = URLSession(configuration: .default)
        session.dataTask(with: URLRequest(url: url)) { data, response, error in
            guard let data, error == nil else {
                print("데이터 로드 실패")
                completion(nil)
                return

 

<T>  제네릭이라 부른다.

 

< T: decodable> ~ (T?) 이 줄에서 (T?)에는 어떤 타입도 들어올 수 있긴하나, 앞쪽에 어떤 T 타입이냐를 물었을 때 Decodable이라고 적어줬기 때문에 Decodable 타입만 준수한다면 어떤 타입이든 들어올 수 있다. 예를 들어 Int, Double, Codable을 채택하고 있는 구조체 등도 가능하다. 

 

@escaping 클로저?

@escaping (T?) -> Void

보통의 코드는 함수 안에서 작성 됐을 때 함수가 끝이나면 함께 책임을 다하고 소멸한다. 하지만 escaping으로 선언한 클로저는

메서드가 끝나서 탈출하더라도, 언제든지 실행이 가능하다는 것을 의미한다.