RxSwift + MVVM์ ์ฌ์ฉํ๋ค๊ฐ RxSwift๊ฐ ์๋ MVVM์ ์ฌ์ฉํ๋ RxSwift ์ ์ฅ์ ์ด ํ์คํ ๋๊ปด์ง๋๊ฑฐ ๊ฐ์์
์ ๋ฒ ํฌ์คํ ์์ MVVM์ ๋ํด ๊ฐ๋จํ๊ฒ ์ ๋ฆฌํด ๋์๋๋ฐ ๐
2022.06.04 - [๐ iOS/Architecture Pattern] - [Swift/Architecture] MVVM ์ดํดํ๊ธฐ
MVVM์ Model - View - ViewModel๋ก ๋๋๋ ๊ฒ์ด ๋ค๊ฐ ์๋ View์ ViewModel์ ์ํธ ์์ฉ์ ๋ํด์ ๋ ๊น๊ฒ ๋ค๋ค ๋ณผ ํ์๊ฐ ์์ด์ ์ ๋ฆฌํด ๋ณด์์ต๋๋ค ๐ฆ
MVC์ ๋ฌธ์ ์
๊ธฐ์กด MVC์ ๊ฒฝ์ฐ Massive ViewController์ ์ฝ์์ด๋ค~ ๋ผ๋ ๋ง์ด ์์ ์ ๋๋ก ViewController์ ์ญํ์ด ์ปธ์ต๋๋ค
UI๋ฅผ ๋ก๋ํ๊ณ ์ฌ์ฉ์์ ์ด๋ฒคํธ๋ฅผ ๋ฐ๊ณ , ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๊ณ ... ๊ทธ๋ฌ๋ค๊ฐ ์ด๋ ๊ฒ ๋ฐฉ๋ํ ViewController ์ญํ์ ์ผ๋ถ๋ฅผ ๋ด๋นํ๋ ViewModel์ด ์๋ MVVM ํจํด์ด ๋ฑ์ฅํ๊ฒ ๋์์ต๋๋ค
MVVM์ด๋
View์์ ์ฌ์ฉ์์ ์ํธ์์ฉํ์ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ผ๋ฉด ์ด ์ฌ์ค์ ViewModel์ ์๋ฆฌ๊ฒ ๋ฉ๋๋ค. ViewModel์ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๊ฒ ๋๋๋ฐ ์ฌ๊ธฐ์๋ ๋คํธ์ํน๋ ํฌํจ๋ฉ๋๋ค
View๋ UI์ ์ ์ ๊ฐ์ ์ํธ์์ฉ์ ์ํ ์ฝ๋๊ฐ ํฌํจ๋๊ณ ViewModel์ ์ด ์ด๋ฒคํธ์ ๋ง๋ ๋น์ฆ๋์ค ๋ก์ง์ ์์ฑํ๊ฒ ๋๋ ๊ฒ์ด์ฃ !
์ด๋ ๊ฒ ํ๋ฉด ViewModel์ด ์ด์ ์ ViewController์์ ์์ฑํ๋ ์ฝ๋๋ฅผ ์ผ๋ถ ๊ฐ์ ธ๊ฐ๋ฉด์ UI์ ๋ ๋ฆฝ์ ์ด๊ธฐ ๋๋ฌธ์ ํ ์คํธ๋ ๋ ์ฌ์ ์ง๋๋ค
ViewModel์ Model์๊ฒ ์ ๋ฐ์ดํธ๋ฅผ ์์ฒญํ๊ณ ๋ฐ์ดํฐ ๋ชจ๋ธ์ ๋ณ๊ฒฝ๋๋ฉด ์ด ์ฌ์ค์ ViewModel์๊ฒ ์๋ฆฝ๋๋ค (์ด๊ฑด ์ฝ๋๋ก ๋ณด๋๊ฒ ๋ ์ข์ต๋๋ด)
MVVM ์์ ์๊ฐํด ๋ณผ ๊ฒ ๐ค
View ๊ฐ ViewModel์ own ํ๊ณ ์๊ธฐ ๋๋ฌธ์ View์์ ViewModel์ ์ ๊ทผ์ด ๊ฐ๋ฅํฉ๋๋ค
๊ทธ๋ฌ๋! ViewModel์ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๊ณ UI๋ฅผ ์ ๋ฐ์ดํธ ํ๊ธฐ ์ํด์ ์ด๋ป๊ฒ View์๊ฒ ์๋ ค์ฃผ์ด์ผ ํ ๊น์?
ViewModel๋ View๋ฅผ own ํ๊ฒ ๋๋ฉด ์ํ ์ฐธ์กฐ๊ฐ ๊ฑธ๋ฆดํ ๋ฐ ๋ง์ด์์
์ด๋ ์ฌ์ฉํด ์ค ์ ์๋๊ฒ ํด๋ก์ , notification, delegate๋ฅผ ์ฌ์ฉํด ์ค ์๋ ์๋๋ฐ Observable ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค (Combine์ด๋ RxSwift๋ฅผ ํด๋ณด์ จ๋ค๋ฉด ์ต์ํ์ค ๊ฑฐ์์!)
ํ์ผ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
๋ชจ๋ ์ฝ๋๋ฅผ ์์ฑํ๊ณ ๋๋ฉด ์ด๋ฐ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ฒ ๋ฉ๋๋ค! ์ฐ์ Service ๋ถํฐ ์งํํ๊ฒ ์ต๋๋ค!
์ ์ฒด ์ฝ๋๋ ์ด ๋งํฌ๋ฅผ ๋๋ฅด์๋ฉด ๋ฉ๋๋ค ๐๐ป ๊นํ๋ธ ๋งํฌ
API์ ๋คํธ์ํน
์ฐ์ ์ฌ์ฉํ api๋ฅผ ์๊ฐํ๊ฒ ์ฉ๋๋ค
https://jsonplaceholder.typicode.com ๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค๐๐ป
๋คํธ์ํน -> ํต์ ์ฑ๊ณต -> json decoding ์ด๋ ๊ฒ ํด์ค ๊ฒ๋๋ค!
/users/2๋ฅผ ํ๋ฉด user id 2์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ json์ผ๋ก ๋ฐํํฉ๋๋ค
๊ตฌ์กฐ๋ฅผ ๋ฏธ๋ฆฌ๋ณด๋ฉด ์ด๋ ์ต๋๋ค! ๋ง์ ์ ๋ณด๋ฅผ ๋ฐํํด ์ฃผ์ง๋ง MVVM์ด ์ฃผ์ธ๊ณต์ด๊ธฐ ๋๋ฌธ์ UI์ ์ฌ์ฉํ ๋ฐ์ดํฐ๋ ๊ฐ๋จํ๊ฒ ์์ฑํด ์ฃผ๊ฒ ์ต๋๋ค
์ด๋ฒ ํฌ์คํ ์์๋ 'username', 'email'๋ง ์ฌ์ฉํด ์ค๋๋ค!
import Foundation
class NetworkManager {
static let shared = NetworkManager()
private init() { }
func getUser(completion: @escaping (Bool, User?) -> ()) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/2") else {
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil else {
completion(false, nil)
print("error ::: \(error!)")
return
}
guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else {
completion(false, nil)
return
}
do {
let model = try JSONDecoder().decode(User.self, from: data!)
completion(true, model)
} catch {
completion(false, nil)
}
}.resume()
}
}
์ด๋ฒ ํฌ์คํ ์์๋ ํ๋์ api ๋ง ์ฌ์ฉํด ์ฃผ๊ธฐ ๋๋ฌธ์ ๋ฒ์ฉ์ ์ผ๋ก ์์ฑํ์ง ์์์ต๋๋ค
NetworkManager๋ Singleton ์ผ๋ก ๋คํธ์ํฌ ํต์ ํ ์ฑ๊ณตํ๋ฉด json์ User ๊ฐ์ฒด๋ก ๋์ฝ๋ฉ ํฉ๋๋ค. ์ด์ User์ ์์ฑํด ์ฃผ๊ฒ ์ต๋๋ค
Model ์์ฑํ๊ธฐ
์ ์ ๋ณด์์ 'username'๊ณผ 'email'๋ง ์ฌ์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๊ฐ๋จํ๊ฒ Model์ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค
import Foundation
struct User: Codable {
enum CodingKeys: String, CodingKey {
case userName = "username"
case email
}
let userName: String
let email: String
}
์ฌ๊ธฐ์ ์ ์ฒด ๋ชจ๋ ๋ฐ์ดํฐ์ ๋ํด ๋ชจ๋ธ ํด๋์ค๋ฅผ ์์ฑํ ํ์๋ ์์ง๋ง
ํน์ ์ ์ฒด ๋ฐ์ดํฐ ๊ตฌ์กฐ์ ๊ดํด Model์ ์์ฑํ๊ณ ์ถ์ผ์ ๊ฒฝ์ฐ ์๋ ์ฝ๋๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์!
// โญ๏ธ api ๋ชจ๋ ์ ๋ณด์ ๋ํ ๋ชจ๋ธ ํด๋์ค
import Foundation
struct User: Codable {
enum CodingKeys: String, CodingKey {
case id, name, email, address, phone, website, company
case userName = "username"
}
let id: Int
let name: String
let userName: String
let email: String
let address: Address
let phone: String
let website: String
let company: Company
}
struct Address: Codable {
enum CodingKeys: String, CodingKey {
case street, suite, city, geo
case zipCode = "zipcode"
}
let street: String
let suite: String
let city: String
let zipCode: String
let geo: Geo
}
struct Geo: Codable {
enum CodingKeys: String, CodingKey {
case lat, lng
}
let lat: String
let lng: String
}
struct Company: Codable {
enum CodingKeys: String, CodingKey {
case name, catchPhrase, bs
}
let name: String
let catchPhrase: String
let bs: String
}
๋คํธ์ํน์ ์ฑ๊ณตํ๋ฉด json api๋ ๋ชจ๋ธ ํด๋์ค์ธ User๋ก ๋์ฝ๋ฉ ๋ฉ๋๋ค. ์ด Model์ด ์ ๋ฐ์ดํธ ๋์ด ViewModel์๊ฒ ์ ๋ฌ ๋๋ ๊ฒ!
์ด์ View๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค
View ์์ฑํ๊ธฐ
import UIKit
class UserViewController: UIViewController {
let nameLabel = UILabel()
let emailLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
nameLabel.backgroundColor = .gray
emailLabel.backgroundColor = .yellow
nameLabel.text = "์ด๋ฆ์
๋๋ค"
emailLabel.text = "์ด๋ฉ์ผ์
๋๋ค"
attachUI()
}
func attachUI() {
view.addSubview(nameLabel)
view.addSubview(emailLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
emailLabel.translatesAutoresizingMaskIntoConstraints = false
emailLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true
emailLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor).isActive = true
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor).isActive = true
}
}
์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ์ด๋ ๊ฒ ํ์ ํ UI๊ฐ ๋ํ๋ฉ๋๋ค ๐ฅฒ
๊ฐ๋จํ๊ฒ ์ฝ๋๋ก ์์ฑํด ๋์์ผ๋ ๋ณต์ฌํด์ ๋ถ์ฌ๋ฃ์ผ์๋ฉด ๋ฉ๋๋ค
UserViewController.swift ์์๋ UI์ ๋ํ ์ฝ๋๊ฐ ์์ต๋๋ค. ์ด๋ ๋คํธ์ํน ํด์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ์ค๊ณ ์ด ๋ฐ์ดํฐ๋ฅผ UI์ ๋ณด์ฌ์ฃผ๋ ค๋ฉด ์ด์ ์ ์์ฑํด ๋์๋ NetworkManager ์ ํธ์ถํด์ ๋คํธ์ํน์ ํด์ผ ํฉ๋๋ค
์ด ๋คํธ์ํน์ ๋น์ฆ๋์ค ๋ก์ง์ผ๋ก MVVM์์๋ UI๋ฅผ ๋ด๋นํ๊ณ ์ ์ ์์ ์ธํฐ๋ ์ ์ ๋ด๋นํ๋ View์ ์ญํ์ด ์๋ ViewModel์ ์ญํ์ ๋๋ค
์ด์ ViewModel์ ์์ฑํด ์ฃผ๊ฒ ์ต๋๋ค
ViewModel ์์ฑํ๊ธฐ
import Foundation
final class UserViewModel {
func fetchData() {
NetworkManager.shared.getUser { [weak self] isSuccess, data in
if isSuccess {
}
}
}
}
UserViewModel.swift ํ์ผ์ ์ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค
ViewModel์ ๋คํธ์ํน์ ์์ํ๋ ํจ์์ธ fetchData()๋ฅผ ํฌํจํฉ๋๋ค
private let viewModel = UserViewModel()
UserViewController.swift์ viewModel ํ๋กํผํฐ๋ฅผ ์ถ๊ฐํด์ค๋๋ค. ์ด์ UserViewController์ viewDidLoad์์ attachUI() ์๋์ ์ฝ๋๋ฅผ ์ถ๊ฐํฉ๋๋ค
viewModel.fetchData()
attachUI() ํ viewModel์ ์๋ fetchData()ํจ์๋ฅผ ์คํํด ๋คํธ์ํน์ ์์ํฉ๋๋ค
ViewController๋ ViewModel์ ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ ViewModel์ ๋น์ฆ๋์ค ๋ก์ง์ ์คํํ์ง๋ง ViewModel์ ์ด๋ป๊ฒ ViewController์๊ฒ ๋คํธ์ํน์ ์ฑ๊ณต์ ์ผ๋ก ๋๋ด๊ณ ์ ๋ฐ์ดํธ ํ Model์ ์ ๋ฌ ํ ์ ์์๊น์?
์ฌ๊ธฐ์ ํด๋ก์ , ๋ธ๋ฆฌ๊ฒ์ดํธ, ๋ ธํฐํผ์ผ์ด์ ์ ์ฌ์ฉํ ์ ์์ง๋ง ObservableObject๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค
ObservableObject ์์ฑํ๊ธฐ
RxSwift์ Observable์ด ์์ต๋๋ค. ์ด๋ฆ์์ ์ ์ถ ๊ฐ๋ฅํ ๋ฏ์ด ๋ณํ ๊ด์ธก ๊ฐ๋ฅํ ๊ฐ์ฒด ์ ๋๋ค
import Foundation
class ObservableObject<T> {
//๐๐ป 1
var value: T {
didSet {
listener?(value)
}
}
//๐๐ป 2
private var listener: ((T) -> Void)?
//๐๐ป 3
init(_ value: T) {
self.value = value
}
//๐๐ป 4
func bind(_ listener: @escaping (T) -> Void ) {
listener(value)
self.listener = listener
}
}
์ฒ์ฒํ ์ดํด๋ณด๊ฒ ์ต๋๋ค!
์ฐ์ 1๋ฒ์ด ์๋ 2๋ฒ ๋ถํฐ ๋ณด๊ฒ ์ต๋๋ค
private var listener: ((T) -> Void)?
listener๋ ํด๋ก์ ์ ๋๋ค. T ๊ฐ์ฒด๋ฅผ ๊ฐ์ง๊ณ ์ฝ๋๋ฅผ ์คํํฉ๋๋ค!
var value: T {
didSet {
listener?(value)
}
}
value๋ ์ ๋ค๋ฆญ์ผ๋ก ๋ค์ํ ํ์ ์ ๊ฐ์ฒด๋ฅผ ๋ฐ์ต๋๋ค. ์ด๋ value์ ๊ฐ์ด ๋ณ๊ฒฝ์ด ๋๋ฉด didSet ๋ด๋ถ์ ์๋ ์ฝ๋๊ฐ ์คํ๋ฉ๋๋ค
ํด๋ก์ ์ธ listener์๊ฒ ๋ณ๊ฒฝ๋ value ๊ฐ์ ์ ๋ฌํ๊ณ listener๋ ์ด value๋ฅผ ๊ฐ์ง๊ณ ๋ด๋ถ์ ์ฝ๋๋ฅผ ์คํํ๊ฒ ๋๋ ๊ฑฐ์ฃ
์ฆ ์ฌ๊ธฐ์ value๊ฐ Observable ์ฆ, ๊ด์ฐฐ ๊ฐ๋ฅํ ๊ฐ์ฒด์ ๋๋ค! Model์ด ์ ๋ฐ์ดํธ ๋๋ฉด -> UI๋ฅผ ์ ๋ฐ์ดํธํ๋ค
์ฌ๊ธฐ์ T๊ฐ Model์ด๊ณ UI ์ ๋ฐ์ดํธ ์ฝ๋๋ listener์ ํฌํจ๋์ด ์๋ ๊ฒ์ ๋๋ค
๊ทธ๋ ๋ค๋ฉด value๋ viewModel์์ ๊ทธ ๊ฐ์ ์ ๋ฐ์ดํธ ํด์ฃผ๊ณ
UI ์ ๋ฐ์ดํธ๋ฅผ ์๋ํ๋ listener๋ View์์ ์ ๊ทผํ๋ฉด => Model(์ฌ๊ธฐ์๋ value) ๋ณ๊ฒฝ ๋๋ฉด์ UI ์ ๋ฐ์ดํธ ์คํ
์ด๋ ๊ฒ ํ ์ ์์ต๋๋ค ๐๐ป
listener๋ View์์ ์ ๊ทผํด์ ์ค์ ํด ์ฃผ๋ฉด listener๊ฐ view ์ ๋ฐ์ดํธ ์ฝ๋๋ฅผ ๊ฐ์ง๊ณ ์๊ฒ ์ฃ ?
View์์ listener๋ฅผ ๋ฐ์ ์ ์๋๋ก
func bind(_ listener: @escaping (T) -> Void ) {
listener(value)
self.listener = listener
}
4๋ฒ ์ฝ๋ ๋ธ๋ก์ ์์ฑํด ์ค์๋ค
init(_ value: T) {
self.value = value
}
3๋ฒ ์ฝ๋ ๋ธ๋ก์ ViewModel์์ ๊ด์ฐฐ ๊ฐ๋ฅํ ๊ฐ์ฒด๋ฅผ ์ค์ ํด ์ค ์ ์๋๋ก ์ด๋์ ๋ผ์ด์ ์์ ํด๋น ๊ฐ์ฒด๋ฅผ ๋ฐ๋ ๋ค๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๋ค์ ViewModel๋ก ๋์๊ฐ๋ณด๊ฒ ์ต๋๋ค
ViewModel ์์ฑํ๊ธฐ
import Foundation
final class UserViewModel {
var data: ObservableObject<User?> = ObservableObject(nil)
func fetchData() {
NetworkManager.shared.getUser { [weak self] isSuccess, data in
if isSuccess {
self?.data.value = data
}
}
}
}
์ต์ ๋นํ User ๊ฐ์ฒด ํ์ ์ value ํ์ ์ผ๋ก ๋ฐ๋ ObservableObject์ธ data ํ๋กํผํฐ๋ฅผ ์ถ๊ฐํด ์ค๋๋ค
๊ทธ ํ ๋คํธ์ํน์ ์ฑ๊ณตํ๋ฉด ObservableObject์ value๋ฅผ ์ ๋ฐ์ดํธ ํด์ค๋๋ค
์ด ํ๋ก์ฐ๋๋ก ์คํ์ด ๋๋ฉด ์ต์ข ์ ์ผ๋ก
var value: T {
didSet {
listener?(value)
}
}
ObservableObject์ value์ didSet ์ฝ๋๊ฐ ์คํ๋๋ฉด์ listener๊ฐ ์คํ๋๊ฒ ์ฃ ?
์ด์ listener์ ์ค์ ํด ์ฃผ๊ฒ ์ต๋๋ค
View ์์ฑํ๊ธฐ
import UIKit
class UserViewController: UIViewController {
private let viewModel = UserViewModel()
let nameLabel = UILabel()
let emailLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
nameLabel.backgroundColor = .gray
emailLabel.backgroundColor = .yellow
nameLabel.text = "์ด๋ฆ์
๋๋ค"
emailLabel.text = "์ด๋ฉ์ผ์
๋๋ค"
setupBinders() // โญ๏ธ 1
attachUI()
viewModel.fetchData()
}
// โญ๏ธ 2
func setupBinders() {
viewModel.data.bind { [weak self] user in
guard let user = user else { return }
DispatchQueue.main.async {
self?.nameLabel.text = user.userName
self?.emailLabel.text = user.email
}
}
}
func attachUI() {
// ์๋ต ...
}
}
์ฐ์ 2๋ฒ ์ฝ๋์ setupBinders() ๋ฉ์๋๋ฅผ ์ถ๊ฐํด ์ค๋๋ค.
์ด ๋ฉ์๋์์๋ viewModel์ ObservableObject์ listener๋ฅผ ์ค์ ํด ์ค๋๋ค. bind๋ฅผ ์ด์ฉํด์ value ๊ฐ์ด ์ ๋ฐ์ดํธ ๋๋ฉด ์คํ๋ listener ํด๋ก์ ๋ฅผ ์์ฑํด ์ค๋๋ค.
์ด๋ ์ ๋ฌ ๋ฐ๋ value ํ์ ์ ViewModel์์ ObservableObject์์ ์ค์ ํด ์ค User ์ ๋๋ค!
user๊ฐ nil์ด ์๋๋ผ๋ฉด UI๋ฅผ ์ ๋ฐ์ดํธ ํด์ค๋๋ค. ์ด๋ ํด๋ก์ ๋ด๋ถ์์ ์คํ๋๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ์ค๋ ๋๋ก ๋ณ๊ฒฝํ๋ ์์ ์ด ํ์ ํฉ๋๋ค.
1๋ฒ ์ฝ๋ setupBinders()๋ฅผ ์ถ๊ฐํด์ฃผ๋ฉด ๋ฉ๋๋ค!
์์ฑ
ํ๋ ์ด์คํ๋๊ฐ ์ด์ง ๋ณด์๋ค๊ฐ ๋คํธ์ํน์ด ๋๋๊ณ UI๊ฐ ์ ๋ฐ์ดํธ ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค!
์ฐธ๊ณ ์ฌ์ดํธ
https://www.youtube.com/watch?v=sbYaWJEAYIY <- ์ ๊ธฐ์ค ์ ๋ง ์ ์ ๋ฆฌํด ๋์ ์์์ธ๊ฑฐ ๊ฐ์ต๋๋ค ๊ผญ ๋ณด์๊ธธ ์ถ์ฒ ๋๋ฆฝ๋๋ค ๐๐ป
๋ฌธ์ ๋ ์ด์ํ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ๋๊ธ ๋ถํ๋๋ ค์๐ฆ
'๐ iOS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Swift] UITextField borderStyle ํ ์คํธ ํ๋ ํ ๋๋ฆฌ ์ค์ (0) | 2022.06.13 |
---|---|
[Swift] MVVMC ๊ฐ๋จํ๊ฒ ์์๋ณด๊ธฐ (0) | 2022.06.11 |
[iOS/Swift] UICollectionView scroll animation (0) | 2022.06.06 |
[iOS/Swift] UICollectionViewFlowLayout ์์๋ณด๊ธฐ (0) | 2022.06.06 |
[Swift] MVVM ๊ฐ๋จํ๊ฒ ์์๋ณด๊ธฐ (0) | 2022.06.04 |
๋๊ธ