์ƒ์„ธ ์ปจํ…์ธ 

๋ณธ๋ฌธ ์ œ๋ชฉ

[ํŒ€ํ”„๋กœ์ ํŠธ 4์ผ์ฐจ] ํ•˜ํ”„๋ชจ๋‹ฌ ๊ตฌํ˜„์— ๋Œ€ํ•œ ๊ณ ๋ฏผ

๋ณธ๋ฌธ

 

์˜ˆ๋งคํ•˜๊ธฐ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ•˜ํ”„๋ชจ๋‹ฌ์„ ๋„์›Œ์•ผํ•˜๋Š” ์ƒํ™ฉ์ด๋‹ค. 

์‚ฌ์‹ค ์ง€๋‚œ ํ‚ค์˜ค์Šคํฌ ์•ฑ์„ ๊ตฌํ˜„ํ•  ๋•Œ์™€๋„ ๋น„์Šทํ•˜๋‹ค. 

๊ทธ์น˜๋งŒ ๊ทธ๋•Œ๋ณด๋‹ค ์ง€๊ธˆ ๊ณ ๋ฏผ์ด ๋˜๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, 

์˜ˆ๋งคํ•˜๊ธฐ ๋ฒ„ํŠผ์€ ๋ทฐ์— ์žˆ๊ณ  ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ addTarget์œผ๋กœ ๋ชจ๋‹ฌ์„ ๋„์šฐ๋ ค๊ณ  ํ•œ๋‹ค. 

์ด๋•Œ, ๋ณ„๋„์˜ ๋ทฐ์ปจ์„ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ํ•จ์ˆ˜ ์•ˆ์—์„œ ๋งŒ๋“ค์–ด ๋ณด๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ

๋ชจ๋‹ฌ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” present ๋ฉ”์„œ๋Š” UIViewController ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ๋ผ 

UIView์—์„œ๋Š” ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜๊ฐ€ ์—†๋‹ค๊ณ  ํ•œ๋‹ค. ์ด๊ฒŒ ๋ฌธ์ œ๊ฐ€ ๋œ ๊ฒƒ์ด๋‹ค...

 

 

 

MovieInfoViewController.swift

 

 

 

 

MovieInfoView.swift

showModal ๋’ค์— ()๋Š” ์˜คํƒ€!

์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. 

 

 

์ด ์˜ค๋ฅ˜๋Š” movieInfoVC ์ธ์Šคํ„ด์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์‹œ์ ์— self๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, reservationButton์˜ addTarget ์„ค์ •์„ init ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค.

addTarget์€ init ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ ์„ค์ •ํ•˜์—ฌ, self๊ฐ€ ์ดˆ๊ธฐํ™”๋œ ํ›„์— movieInfoVC ์ธ์Šคํ„ด์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

 

 

์ด๋ ‡๊ฒŒ ์œ„์น˜๋ฅผ ๋ฐ”๊ฟ” ์ฃผ์—ˆ๋”๋‹ˆ ์˜ค๋ฅ˜๋Š” ์—†์–ด์กŒ์œผ๋‚˜ ํ™”๋ฉด์ด ์›€์ง์ด์ง€๊ฐ€ ์•Š๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋‹ค์‹œ ๋ฐœ์ƒ...

 

 

MovieInfoView์™€ MovieInfoViewController ์‚ฌ์ด์— ๋ช…ํ™•ํ•œ ์—ญํ•  ๊ตฌ๋ถ„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. MovieInfoView์—์„œ ๋ชจ๋‹ฌ์„ ์ง์ ‘ ๋„์šฐ๋Š” ๊ฒƒ์€ ๋ฐ”๋žŒ์งํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์•ก์…˜์„ ์„ค์ •ํ•  ๋•Œ MovieInfoViewController์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋Œ€์‹ , ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋˜๋Š” ํด๋กœ์ €๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ์ถ”๊ฐ€) gpt๊ฐ€ ์ด๋ ‡๊ฒŒ ๋งํ•œ ๊ฑด ์ด ๊ตฌ์กฐ๊ฐ€ ์ž˜๋ชป๋œ ์ด์œ ๋Š”

๋ทฐ์ปจ์—์„œ ๋ทฐ๋ฅผ, ๋ทฐ์—์„œ ๋ทฐ์ปจ์„ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ๋Š” ๊ตฌ์กฐ์˜€๋˜ ๊ฒƒ...

์ด๋ ‡๊ฒŒ ํ•˜๋‹ˆ๊นŒ ์ž‘๋™์ด ์ž˜ ๋œ๋‹ค..ใ…Ž

 

 

์•„๋ž˜๋Š” ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ์™€ ํด๋กœ์ €๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค.


UIView ๋‚ด์—์„œ UIButton์˜ ์•ก์…˜์„ ์„ค์ •ํ•˜๊ณ  ๋‹ค๋ฅธ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์™€ ์ƒํ˜ธ์ž‘์šฉ์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ช‡ ๊ฐ€์ง€ ๋””์ž์ธ ํŒจํ„ด๊ณผ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด, ํด๋กœ์ € ์ฝœ๋ฐฑ, ๊ทธ๋ฆฌ๊ณ  ๋…ธํ‹ฐํ”ผ์ผ€์ด์…˜ ์„ผํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•˜๊ณ , ์ฝ”๋“œ ์˜ˆ์‹œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

 

1. ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด์€ ํ•œ ๊ฐ์ฒด๊ฐ€ ๋‹ค๋ฅธ ๊ฐ์ฒด์—๊ฒŒ ์ž‘์—…์„ ์œ„์ž„ํ•˜๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด UIView์™€ UIViewController ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

1. ํ”„๋กœํ† ์ฝœ ์ •์˜:

protocol MovieInfoViewDelegate: AnyObject {
    func didTapReservationButton()
}

 

 

2. ๋ทฐ ํด๋ž˜์Šค์— ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€:

class MovieInfoView: UIView {
    weak var delegate: MovieInfoViewDelegate?
    
    // ์˜ˆ์•ฝํ•˜๊ธฐ ๋ฒ„ํŠผ
    var reservationButton: UIButton = {
        let button = UIButton()
        button.setTitle("์˜ˆ๋งคํ•˜๊ธฐ", for: .normal)
        button.setTitleColor(.mainWhite, for: .normal)
        button.backgroundColor = .mainRed
        button.layer.cornerRadius = 8
        button.addTarget(self, action: #selector(reservationButtonTapped), for: .touchUpInside)
        return button
    }()
    
    @objc
    private func reservationButtonTapped() {
        delegate?.didTapReservationButton()
    }
}

 

 

3. ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์„ค์ • ๋ฐ ๊ตฌํ˜„:

class YourViewController: UIViewController, MovieInfoViewDelegate {
    private let movieInfoView = MovieInfoView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(movieInfoView)
        movieInfoView.snp.makeConstraints { $0.edges.equalToSuperview() }
        movieInfoView.delegate = self
    }
    
    func didTapReservationButton() {
        let modalVc = ModalViewController()
        if let modal = modalVc.sheetPresentationController {
            modal.detents = [.medium()]
        }
        present(modalVc, animated: true)
    }
}

 

 

 

2. ํด๋กœ์ € ์ฝœ๋ฐฑ

ํด๋กœ์ €๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ก์…˜์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ˆ˜ํ–‰ํ•  ์ž‘์—…์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

ํด๋กœ์ € ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€

class MovieInfoView: UIView {
    var onReservationButtonTapped: (() -> Void)?
    
    // ์˜ˆ์•ฝํ•˜๊ธฐ ๋ฒ„ํŠผ
    var reservationButton: UIButton = {
        let button = UIButton()
        button.setTitle("์˜ˆ๋งคํ•˜๊ธฐ", for: .normal)
        button.setTitleColor(.mainWhite, for: .normal)
        button.backgroundColor = .mainRed
        button.layer.cornerRadius = 8
        button.addTarget(self, action: #selector(reservationButtonTapped), for: .touchUpInside)
        return button
    }()
    
    @objc
    private func reservationButtonTapped() {
        onReservationButtonTapped?()
    }
}

 

 

๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ํด๋กœ์ € ์„ค์ •

class YourViewController: UIViewController {
    private let movieInfoView = MovieInfoView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(movieInfoView)
        movieInfoView.snp.makeConstraints { $0.edges.equalToSuperview() }
        
        movieInfoView.onReservationButtonTapped = { [weak self] in
            let modalVc = ModalViewController()
            if let modal = modalVc.sheetPresentationController {
                modal.detents = [.medium()]
            }
            self?.present(modalVc, animated: true)
        }
    }
}

 

 

 

์š”์•ฝ

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด: ๋ทฐ์™€ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ฐ„์˜ ๋ช…ํ™•ํ•œ ์—ญํ•  ๋ถ„๋ฆฌ๋ฅผ ์œ ์ง€ํ•˜๊ณ , ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

ํด๋กœ์ € ์ฝœ๋ฐฑ: ๊ฐ„๋‹จํ•˜๊ณ  ์ง๊ด€์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ทฐ์™€ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 


Q1: ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•  ๋•Œ, ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ weak๋กœ ์„ ์–ธํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•  ๋•Œ, ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ weak๋กœ ์„ ์–ธํ•˜๋Š” ์ด์œ ๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ์ž…๋‹ˆ๋‹ค.

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด์—์„œ๋Š” ๊ฐ์ฒด ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์ด ์ฃผ๋กœ ์ผ๋Œ€์ผ๋กœ ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, MovieInfoView๊ฐ€ MovieInfoViewController์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๊ฐ€ ๊ฐ•ํ•œ ์ฐธ์กฐ(strong reference)๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋˜๋ฉด, ๋‘ ๊ฐ์ฒด ๊ฐ„์— ์ˆœํ™˜ ์ฐธ์กฐ(circular reference)๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆœํ™˜ ์ฐธ์กฐ๋Š” ๋‘ ๊ฐ์ฒด๊ฐ€ ์„œ๋กœ๋ฅผ ๊ฐ•ํ•˜๊ฒŒ ์ฐธ์กฐํ•˜๊ณ  ์žˆ์–ด ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋˜์ง€ ์•Š๋Š” ์ƒํ™ฉ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ์•ฝํ•œ ์ฐธ์กฐ(weak reference)๋กœ ์„ ์–ธํ•˜์—ฌ, ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๊ฐ์ฒด๊ฐ€ ๋” ์ด์ƒ ํ•„์š” ์—†์„ ๋•Œ ์ ์ ˆํžˆ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

weak var delegate: MovieInfoViewDelegate?

 

 

 

Q2: ํด๋กœ์ € ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์˜ ์ฐจ์ด์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด๊ณผ ํด๋กœ์ € ์ฝœ๋ฐฑ์€ ๊ฐ์ฒด ๊ฐ„์˜ ํ†ต์‹ ์„ ์œ„ํ•ด ์‚ฌ์šฉ๋˜์ง€๋งŒ, ๊ทธ ์‚ฌ์šฉ ๋ฐฉ์‹๊ณผ ๋ชฉ์ ์—๋Š” ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ํŒจํ„ด

๋ชฉ์ : ์ฃผ๋กœ ๊ฐ์ฒด ๊ฐ„์˜ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

๊ตฌ์กฐ: ํ•˜๋‚˜์˜ ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๊ณ , ์ด๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ์ฒด์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์˜ˆ: UITableViewDelegate, UICollectionViewDelegate ๋“ฑ.

์žฅ์ :

์ฝ”๋“œ์˜ ๋ชจ๋“ˆ์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ ๋ฉ”์„œ๋“œ๋ฅผ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์–ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ :

์„ค์ •์ด ๋‹ค์†Œ ๋ฒˆ๊ฑฐ๋กœ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํ•œ ์ž‘์—…์— ๋Œ€ํ•ด ๊ณผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

 

 

ํด๋กœ์ € ์ฝœ๋ฐฑ

// ํด๋กœ์ € ์ฝœ๋ฐฑ ์˜ˆ์‹œ
networkManager.fetchData { result in
    switch result {
    case .success(let data):
        // ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
    case .failure(let error):
        // ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
    }
}

๋ชฉ์ : ์ฃผ๋กœ ๋‹จ์ผ ์ž‘์—…์ด๋‚˜ ๋น„๋™๊ธฐ ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

๊ตฌ์กฐ: ํ•จ์ˆ˜๋‚˜ ๋ฉ”์„œ๋“œ์˜ ์ธ์ˆ˜๋กœ ํด๋กœ์ €๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์˜ˆ: ๋„คํŠธ์›Œํฌ ์š”์ฒญ์˜ ์™„๋ฃŒ ํ•ธ๋“ค๋Ÿฌ, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฃŒ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ.

์žฅ์ :

๋‹จ์ผ ์ž‘์—…์— ๋Œ€ํ•ด ๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ค์ •์ด ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ :

์—ฌ๋Ÿฌ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๊ฒฝ์šฐ ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ์— ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (ํŠนํžˆ [weak self] ์บก์ฒ˜ ๋ชฉ๋ก ์‚ฌ์šฉ).

 

 

๊ด€๋ จ๊ธ€ ๋”๋ณด๊ธฐ