/BeMeiOS

๐Ÿ“ ๋‚˜์— ๋Œ€ํ•ด ์•Œ์•„๊ฐ€๋Š” ์งˆ๋ฌธ ๋‹ค์ด์–ด๋ฆฌ, BeMe

Primary LanguageSwift

BeMeiOS

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

  • SOPT 27th ์žฅ๊ธฐ ํ•ด์ปคํ†ค APPJAM
  • ๊ธฐ๊ฐ„: 2020.12.26 ~ 2021.1.16

๋งค์ผ ์งˆ๋ฌธ์— ๋‹ต์„ ํ•˜๋ฉฐ ๋‚˜๋ฅผ ์•Œ์•„๊ฐ€๋Š” ์งˆ๋ฌธ๋‹ค์ด์–ด๋ฆฌ BeMe ์ž…๋‹ˆ๋‹ค.

24์‹œ๊ฐ„๋งˆ๋‹ค ์ƒˆ๋กœ์šด ์งˆ๋ฌธ ์ œ๊ณต, ์ƒˆ๋กœ์šด ์งˆ๋ฌธ ๋ฐ›๊ธฐ๋ฅผ ํ†ตํ•ด ์งˆ๋ฌธ์— ๋‹ต์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์‹ ์˜ ๊ธ€์„ ์ „์ฒด ๊ณต๊ฐœํ•˜์—ฌ ์‚ฌ๋žŒ๋“ค๊ณผ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์œผ๋กœ ๋งˆ์ดํŽ˜์ด์ง€์—์„œ ๋‚ด๊ฐ€ ์ง€๊ธˆ๊นŒ์ง€ ๋‹ตํ•œ ์งˆ๋ฌธ์„ ์‰ฝ๊ฒŒ ๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‚ด ์ƒ๊ฐ์ด ์–ด๋–ป๊ฒŒ ๋ณ€ํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๊ฐœ๋ฐœ ํ™˜๊ฒฝ

Swift 4 Xcode swift iOS COCOAPODS

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Library) ๋ชฉ์ (Purpose) ๋ฒ„์ „(VersionA
Alamofire ์„œ๋ฒ„ ํ†ต์‹  Alamofire
Kingfisher ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ Kingfisher
SnapKit ์˜คํ† ๋ ˆ์ด์•„์›ƒ Kingfisher
Then ์งง์€ ์ฝ”๋“œ ์ฒ˜๋ฆฌ Kingfisher
lottie-ios ๋กœํ‹ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ lottie-ios
Firebase/Messaging Firebase Cloud Messaging Kingfisher
SwiftLint ๊น”๋”ํ•œ ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜ Kingfisher

AutoLayout ์ ์šฉ ์—ฌ๋ถ€

1. iPhone 12 Pro ์ ์šฉ

2. iPhone 12 mini ์ ์šฉ

3. iPhone SE2 ์ ์šฉ

๐Ÿ“Œ ์„œ๋น„์Šค workflow

๐Ÿ“Œ ํ˜‘์—… ๋ฐฉ์‹

๐Ÿ“Œ ๊ธฐ๋Šฅ๋ณ„ ๊ฐœ๋ฐœ ์—ฌ๋ถ€

ํ™”๋ฉด ๊ธฐ๋Šฅ ์ƒ์„ธ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ
์Šคํ”Œ๋ž˜์‹œ ์œค์žฌ๐Ÿš€
์˜จ๋ณด๋”ฉ ์œค์žฌ๐Ÿš€
ํšŒ์›๊ฐ€์ž… ์œค์žฌ๐Ÿš€
๋กœ๊ทธ์ธ ๋กœ๊ทธ์ธ ์œค์žฌ๐Ÿš€
์ž๋™๋กœ๊ทธ์ธ ์œค์žฌ๐Ÿš€
ํ™ˆ ์˜ค๋Š˜์˜ ์งˆ๋ฌธ ๋ฐ ๋‹ต๋ณ€ ์—ด๋žŒ ์œค์žฌ๐Ÿš€
๊ณผ๊ฑฐ์˜ ์งˆ๋ฌธ ๋ฐ ๋‹ต๋ณ€ ์—ด๋žŒ ์œค์žฌ๐Ÿš€
์ถ”๊ฐ€ ์งˆ๋ฌธ ์—ด๋žŒ ์œค์žฌ๐Ÿš€
๋‹ต๋ณ€ ๊ณต๊ฐœ ๋ฒ”์œ„ ์„ค์ • ์œค์žฌ๐Ÿš€
๋‹ต๋ณ€ ์ˆ˜์ • ๋ฐ ์‚ญ์ œ ์œค์žฌ๐Ÿš€
ํƒ์ƒ‰ ๋‚˜์™€ ๋‹ค๋ฅธ ๋‹ต๋ณ€๋“ค ์—ด๋žŒ ์žฌ์šฉ๐Ÿถ
๋‹ค๋ฅธ ๋‹ต๋ณ€๋“ค ๋‘˜๋Ÿฌ๋ณด๊ธฐ ์ตœ์‹ , ํฅ๋ฏธ ํƒญ ๋ฐ ํ‚ค์›Œ๋“œ ํ•„ํ„ฐ ๊ฒ€์ƒ‰ ์žฌ์šฉ๐Ÿถ
์Šคํฌ๋žฉ/ ์–ธ์Šคํฌ๋žฉ ์žฌ์šฉ๐Ÿถ
์‹ ๊ณ ํ•˜๊ธฐ ์žฌ์šฉ๐Ÿถ
ํŒ”๋กœ์ž‰ ํŒ”๋กœ์ž‰/ ํŒ”๋กœ์›Œ์˜ ๋‹ต๋ณ€ ์—ด๋žŒ ์œค์žฌ๐Ÿš€
ํŒ”๋กœ์ž‰/ ํŒ”๋กœ์›Œ ์—ด๋žŒ ์œค์žฌ๐Ÿš€
ํŒ”๋กœ์ž‰/ ํŒ”๋กœ์›Œ ๊ฒ€์ƒ‰ ์‚ฌ์šฉ์ž ๊ฒ€์ƒ‰ ์œค์žฌ๐Ÿš€
ํŒ”๋กœ์šฐ/ ์–ธํŒ”๋กœ์šฐ ์œค์žฌ๐Ÿš€
ํŒ”๋กœ์›Œ ์‚ญ์ œ ์œค์žฌ๐Ÿš€
์‹ ๊ณ ํ•˜๊ธฐ ์œค์žฌ๐Ÿš€
๋งˆ์ดํŽ˜์ด์ง€ ํ”„๋กœํ•„ ํŽธ์ง‘ ์œค์žฌ๐Ÿš€
๋‚ด ๊ธ€, ์Šคํฌ๋žฉ ์—ด๋žŒ ์„ธ๋ž€๐ŸŒฟ
ํ‚ค์›Œ๋“œ ๋ฐ ํ•„ํ„ฐ ๊ฒ€์ƒ‰ ์„ธ๋ž€๐ŸŒฟ
๋‹ต๋ณ€ ๊ณต๊ฐœ ๋ฒ”์œ„ ์„ค์ • ์„ธ๋ž€๐ŸŒฟ
์Šคํฌ๋žฉ/ ์–ธ์Šคํฌ๋žฉ ์„ธ๋ž€๐ŸŒฟ
๊ธ€ ์ƒ์„ธ ๋‹ต๋ณ€ ๋‹ต๋ณ€ ์ƒ์„ธ ์—ด๋žŒ ์žฌ์šฉ๐Ÿถ
๋‹ต๋ณ€ ์ˆ˜์ • ๋ฐ ์‚ญ์ œ ์žฌ์šฉ๐Ÿถ
์Šคํฌ๋žฉ/์–ธ์Šคํฌ๋žฉ ์žฌ์šฉ๐Ÿถ
์‹ ๊ณ ํ•˜๊ธฐ ์žฌ์šฉ๐Ÿถ
๋Œ“๊ธ€ ๋Œ“๊ธ€ ์ž‘์„ฑ ์žฌ์šฉ๐Ÿถ
๋Œ“๊ธ€์˜ ๋‹ต๊ธ€ ์ž‘์„ฑ ์žฌ์šฉ๐Ÿถ
๋Œ“๊ธ€ ๊ณต๊ฐœ๋ฒ”์œ„ ์„ค์ • ์žฌ์šฉ๐Ÿถ
๋Œ“๊ธ€ ์ˆ˜์ • ๋ฐ ์‚ญ์ œ ์žฌ์šฉ๐Ÿถ
๊ธ€ ์“ฐ๊ธฐ ๊ณต๊ฐœ๋ฒ”์œ„ ๋ฐ ๋Œ“๊ธ€ ๊ธฐ๋Šฅ ์„ค์ • ์œค์žฌ๐Ÿš€, ์„ธ๋ž€๐ŸŒฟ
์ž„์‹œ์ €์žฅ ์„ธ๋ž€๐ŸŒฟ
๋‹ต๋ณ€ํ•˜๊ธฐ ์„ธ๋ž€๐ŸŒฟ
ํƒ€์ธํŽ˜์ด์ง€ ํŒ”๋กœ์šฐ/ ์–ธํŒ”๋กœ์šฐ ์„ธ๋ž€๐ŸŒฟ
์Šคํฌ๋žฉ/ ์–ธ์Šคํฌ๋žฉ ์„ธ๋ž€๐ŸŒฟ
์‹ ๊ณ ํ•˜๊ธฐ ์„ธ๋ž€๐ŸŒฟ
์ตœ๊ทผ ํ™œ๋™ ๋Œ“๊ธ€ ๋ฐ ๋‹ต๊ธ€ ์•Œ๋ฆผ
ํŒ”๋กœ์›Œ ์•Œ๋ฆผ
์žฅ๊ธฐ ํ‘ธ์‰ฌ ์•Œ๋ฆผ ์˜ค๋Š˜์˜ ์งˆ๋ฌธ ๋ฐ›๊ธฐ ์œค์žฌ๐Ÿš€

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • ๊ธ€์“ฐ๊ธฐ

answer data๋ฅผ ๋„ฃ์–ด์„œ ๊ธ€์„ ๋“ฑ๋กํ•˜๋Š” ์ฝ”๋“œ

AnswerRegistService.shared.regist(answerID: answerData!.id!, content: answerData!.answer!, commentBlocked: commentSwitch.isOn, isPublic: answerSwitch.isOn) {(networkResult) -> (Void) in
    switch networkResult{
    case .success(let data) :
        print(self.isFromFollowingTab)
        if self.isFromFollowingTab {
            self.followScrapButtonDelegate?.fromAnswerVC(indexInVC: self.indexInFollowingVC!)
        }
    case .requestErr(let msg):
        if let message = msg as? String {
            print(message)
        }
    case .pathErr :
        print("pathErr")
    case .serverErr :
        print("serverErr")
    case .networkFail:
        print("networkFail")
        
    }
}
  • ํƒ€์ธ์ด ์“ด ๊ธ€ ๋ณด๊ธฐ

๋‹ค๋ฅธ ํŽ˜์ด์ง€๋ฅผ ๋‹ค๋…€์™€์„œ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ๊ธ€์— ๋ณ€๋™์ด ์ƒ๊ธธ ๊ฒฝ์šฐ ์ƒˆ๋กœ ํ†ต์‹ ์„ ํ•ด์„œ Reloadํ•ด์ค˜์•ผํ•จ ํŽ˜์ด์ง• ํ•ด์„œ ์ •๋ณด๋ฅผ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์— 1ํŽ˜์ด์ง€๋ถ€ํ„ฐ ์›๋ž˜์˜ ํŽ˜์ด์ง€๊นŒ์ง€ ๋‹ค์‹œ ๋ฐ›์•„์™€์•ผ ํ•จ ์ด ๊ณผ์ •์—์„œ ์ฒ˜์Œ์—๋Š” for๋ฌธ์„ ์ด์šฉํ•ด์„œ ์–ด๋ ค์›€์„ ๊ฒช์—ˆ์œผ๋‚˜, ํ†ต์‹ ์—์„œ์˜ ์žฌ๊ท€ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ํ†ตํ•ด ํ•ด๊ฒฐ

func getUpdateAnswers(){
    let loadingFrame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
    LoadingHUD.show(loadingFrame: loadingFrame,color: .white)
    curPage = 0
    answers = []
    updateDataOnePage()
}

func updateDataOnePage(){
    
    curPage += 1
    let loadingFrame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
    FollowingGetAnswersService.shared.getAnswerData(page: curPage){(networkResult) -> (Void) in
        switch networkResult{
        case .success(let data) :
            if let answerDatas = data as? FollowingAnswersData {
                self.pageLen = answerDatas.pageLen
                for answerData in answerDatas.answers{
                    self.answers.append(answerData)
                }
                
            }
            if self.curPage < self.answerPage {
                self.updateDataOnePage()
            }
            else{
                DispatchQueue.global(qos: .background).sync {
                    self.wholeCollectionView.reloadData()
                    self.wholeCollectionView.setContentOffset(CGPoint(x: 0, y: self.lastY), animated: false)
                }
                
                LoadingHUD.hide()
                if self.answers.count == 0{
                    self.customEmptyView.setItems(text: "์•„์ด์ฟ ..! ์•„์ง ํŒ”๋กœ์šฐํ•˜๋Š” ์‚ฌ๋žŒ์ด ์—†๊ตฐ์š”\nํŒ”๋กœ์ž‰์„ ํ•˜๊ณ  ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์˜ ๊ธ€์„ ๋‘˜๋Ÿฌ๋ณด์„ธ์š”")
                    self.wholeCollectionView.addSubview(self.customEmptyView)
                    print(self.customEmptyView.superview?.bounds.minX)
                    self.customEmptyView.snp.makeConstraints{
                        $0.centerX.equalToSuperview()
                        $0.top.equalToSuperview().offset(362)
                        $0.height.equalTo(80)
                    }
                   
    
                }
                else{
                    self.customEmptyView.removeFromSuperview()
                }
    
              
            }

            print("success")

        case .requestErr(let msg):
            if let message = msg as? String {
                print(message)
            }
        case .pathErr :
            print("pathErr")
        case .serverErr :
            print("serverErr")
        case .networkFail:
            print("networkFail")
            
        }
        
        
    }
    
    
}

๐Ÿ“Œ Extension์„ ํ†ตํ•œ ๋ฉ”์†Œ๋“œ ์„ค๋ช…

  • UIView Extension

    extension UIView {
        // Set Rounded View
        func makeRounded(cornerRadius : CGFloat?){
            
            // UIView ์˜ ๋ชจ์„œ๋ฆฌ๊ฐ€ ๋‘ฅ๊ทผ ์ •๋„๋ฅผ ์„ค์ •
            if let cornerRadius_ = cornerRadius {
                self.layer.cornerRadius = cornerRadius_
            }  else {
                // cornerRadius ๊ฐ€ nil ์ผ ๊ฒฝ์šฐ์˜ default
                self.layer.cornerRadius = self.layer.frame.height / 2
            }
            
            self.layer.masksToBounds = true
        }
        
        // Set UIView's Shadow
        func dropShadow(color: UIColor, offSet: CGSize, opacity: Float, radius: CGFloat) {
            
            // ๊ทธ๋ฆผ์ž ์ƒ‰์ƒ ์„ค์ •
            layer.shadowColor = color.cgColor
            // ๊ทธ๋ฆผ์ž ํฌ๊ธฐ ์„ค์ •
            layer.shadowOffset = offSet
            // ๊ทธ๋ฆผ์ž ํˆฌ๋ช…๋„ ์„ค์ •
            layer.shadowOpacity = opacity
            // ๊ทธ๋ฆผ์ž์˜ blur ์„ค์ •
            layer.shadowRadius = radius
            // ๊ตฌ๊ธ€๋ง ํ•ด๋ณด์„ธ์š”!
            layer.masksToBounds = false
            
        }
        
        // Set UIView's Border
        func setBorder(borderColor : UIColor?, borderWidth : CGFloat?) {
            
            // UIView ์˜ ํ…Œ๋‘๋ฆฌ ์ƒ‰์ƒ ์„ค์ •
            if let borderColor_ = borderColor {
                self.layer.borderColor = borderColor_.cgColor
            } else {
                // borderColor ๋ณ€์ˆ˜๊ฐ€ nil ์ผ ๊ฒฝ์šฐ์˜ default
                self.layer.borderColor = UIColor(red: 205/255, green: 209/255, blue: 208/255, alpha: 1.0).cgColor
            }
            
            // UIView ์˜ ํ…Œ๋‘๋ฆฌ ๋‘๊ป˜ ์„ค์ •
            if let borderWidth_ = borderWidth {
                self.layer.borderWidth = borderWidth_
            } else {
                // borderWidth ๋ณ€์ˆ˜๊ฐ€ nil ์ผ ๊ฒฝ์šฐ์˜ default
                self.layer.borderWidth = 1.0
            }
        }
    }
  • UIViewController

    extension UIViewController {
        // ํ† ์ŠคํŠธ ๋ฉ”์„ธ์ง€
        func showToast(text: String, completion: @escaping ()->()) {
            let toast = ToastView(frame: CGRect(x: 0, y: 0, width: 343, height: 84))
            toast.setLabel(text: text)
            toast.alpha = 0
            self.view.addSubview(toast)
            toast.snp.makeConstraints {
                $0.leading.equalToSuperview().offset(16)
                $0.trailing.equalToSuperview().offset(-16)
                $0.bottom.equalToSuperview().offset(-101)
            }
            UIView.animate(withDuration: 0.3, animations: {
                toast.alpha = 1
                
            },completion: { finish in
                UIView.animate(withDuration: 0.3, delay: 0.7, animations: {
                    toast.alpha = 0
    
                }, completion: { finish in
                    if finish {
                        toast.removeFromSuperview()
                        completion()
                    }
                })
            })
        }
    }
  • UIImageView

    // Kingfisher๋ฅผ ์ด์šฉํ•˜์—ฌ url๋กœ๋ถ€ํ„ฐ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” extension
    extension UIImageView {
        
        public func imageFromUrl(_ urlString: String?) {
            if let url = urlString {
                self.kf.setImage(with: URL(string: url), options: [.transition(ImageTransition.fade(0.5))])
            }
        }
    }

๐Ÿ“Œ ํŒ€์› ์†Œ๊ฐœ

์œค์žฌ ์„ธ๋ž€ ์žฌ์šฉ
๋„ค์ด์Šค

๋ชฉ์ฐจ