iOS MKMagneticProgress를 사용해 circleProgressBar 만들어보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


반원 짜리 모양을 가진 프로그래스 뷰를 만들고 싶었습니다.
앞서 헀던 방식으로 직접 뷰를 그릴수도 있지만, 이번에는 라이브러리를 통해서 만들어보려고 합니다.

MKMagneticProgress를 사용해 circleProgressBar 만들어보기

MKMagneticProgress

import MKMagneticProgress

class HomeTableViewCell: UITableViewCell {
    @IBOutlet weak var circleProgressBar: MKMagneticProgress!

    override func awakeFromNib() {
        super.awakeFromNib()

        setProgressViewUI()
    }
    
    func setProgressViewUI() {
        circleProgressBar.setProgress(progress: 0)

        circleProgressBar.progressShapeColor = UIColor(named: "7A7BDA")!
        circleProgressBar.backgroundShapeColor = (UIColor(named: "FFFFFF")?.withAlphaComponent(0.1))!
        circleProgressBar.titleColor = UIColor.red
        circleProgressBar.percentColor = UIColor.white
        
        
        circleProgressBar.lineWidth = 16
        circleProgressBar.orientation = .bottom
        circleProgressBar.lineCap = .round
    }
}

무척 간단합니다.
여기서 중요한 점은 setProgress에서 progress로 받는 숫자는 0~1사이의 숫자입니다.

전체를 1로 보고 그 사이의 숫자들을 표현해주고 있는 것을 의미합니다.
따라서 해당하는 progress에 유의미한 그래프를 보여주고 싶다면 반드시 0과 1사이의 숫자로 표현해서 보여줘야함을 잊지말아야 합니다!

iOS UIBezierPath를 사용해 custom circleProgressBar 만들어보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


UIBezierPath를 사용해 custom circleProgressBar 만들어보기

우선 view 파일을 하나를 만들어줍니다.

import UIKit

class CircleProgressBarView: UIView {
    // 전체 원을 그려줄 layer
    private var circleLayer = CAShapeLayer()
    // 프로그래스 원을 그려줄 layer
    private var progressLayer = CAShapeLayer()
    
    // 원의 시작과 끝을 그려줄 변수 
    private var startPoint = CGFloat(-Double.pi / 2)
    private var endPoint = CGFloat(3 * Double.pi / 2)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        createCirclePath()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    func createCirclePath() {
        let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: 120, startAngle: startPoint, endAngle: endPoint, clockwise: true)

        // 전체 원 setUI
        circleLayer.path = circlePath.cgPath
        circleLayer.fillColor = UIColor.clear.cgColor
        circleLayer.lineCap = .round
        circleLayer.lineWidth = 10.0
        circleLayer.strokeStart = 0.0
        circleLayer.strokeEnd = 1.0
        circleLayer.strokeColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor
        
        layer.addSublayer(circleLayer)
        
        // 프로그래스 원 UI
        progressLayer.path = circlePath.cgPath
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.lineCap = .round
        progressLayer.lineWidth = 10.0
        progressLayer.strokeStart = 0
        progressLayer.strokeEnd = 0
        progressLayer.strokeColor = UIColor(named: "7A7BDA")?.cgColor
        
        layer.addSublayer(progressLayer)
    
    }

    // 프로그래스 원에 애니메이션을 주고싶다면 사용 
    func progressAnimation(duration: TimeInterval, value: Float, maxValue: Float) {
        let circleProgressAnimation = CABasicAnimation(keyPath: "strokeEnd")
    
        circleProgressAnimation.duration = duration
        circleProgressAnimation.fromValue = 0
        
        progressLayer.strokeStart = 0
        progressLayer.strokeEnd = CGFloat(value/maxValue)
        progressLayer.add(circleProgressAnimation, forKey: "progressAnim")
    }
}

일반적으로 progressAnimation함수에는 maxValue가 들어가있지 않은 예제만 있을 것이다.
근데 저는 전체 원의 최대값을 만들어 그 최대값 대비 value를 넣어줄 예정이기에 인자를 하나 더 받았습니다.ㅎㅎ

이렇게 뷰를 만들었다면 이제 이 해당 뷰를 사용하는 곳에서 사용을 해줘야겠죠?

저는 테이블뷰 셀에 해당 뷰를 만들어주고 있는데, 그러다 보니 뷰의 .center가 잘 맞지 않는 이슈가 있었습니다.
이를 해결하기 위해 snapKit을 사용해 뷰의 위치를 다시 잡아주었습니다.

HomeTableViewCell

import UIKit
import SnapKit

class ProgressTableViewCell: UITableViewCell {
    @IBOutlet weak var containerView: UIView!
    
    var circleProgressView: CircleProgressBarView!
    var circleDuration: TimeInterval = 2

    var stepData = Int()

    override func awakeFromNib() {
        super.awakeFromNib()

        setupCircleProgressBarView()
    }

    func setupCircleProgressBarView() {
        circleProgressView = CircleProgressBarView()
        self.containerView.addSubview(circleProgressView)

        circleProgressView.snp.makeConstraints { make in
            make.centerX.centerY.equalTo(self.containerView)
            make.top.equalTo(self.containerView).inset(165)
        }
        
        self.circleDuration = 2
        self.circleProgressView.progressAnimation(duration: circleDuration, value: Float(self.stepData), maxValue: 10000)
        
        self.containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap)))
        self.circleProgressView.layoutIfNeeded()
    }
    
    @objc func handleTap() {
        self.circleDuration = 2
        self.circleProgressView.progressAnimation(duration: circleDuration, value: Float(self.stepData), maxValue: 10000)
    }
}

iOS CoreMotion 사용해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


CoreMotion

걸음수 데이터를 가져오기 위한 방법은 두가지가 있습니다.

  1. Coremotion
  2. HealthKit

그치만 걸음수 데이터를 무엇을 위해, 어떻게 가져올지에 따라서 둘 중 한가지 방법을 선택해야 합니다.

우선 Coremotion은 실시간 걸음 데이터를 제공합니다.
일반적인 만보기 어플을 구현하기 위해서는 보통 coremotion을 사용합니다.

반면 Healthkit은 애플에서 제공해주는 건강앱에 표시되는 모든 건강데이터들을 그대로 가져옵니다.

즉, 실시간으로 걸음수 데이터를 수집할 목적이라면 coremotion을 사용하고 굳이 실시간 데이터가 필요하지않다면 healthkit을 사용해도 괜찮습니다.

이번에는 실시간으로 걸음수 데이터를 가져오는 coremotion 사용 방법에 대해서 설명해보겠습니다.

Info.plist

우선 걸음수 데이터를 가져오기 위해 유저에게 접근권한요청을 해야합니다.
plist에 다음과 같은 설정을 추가해줍니다.

NSMotionUsageDescription = 걸음수 데이터 측정을 위해 데이터 접근 권한이 필요합니다.

앞서 HealthKit 때도 말씀드렸듯이 이때 접근권한 메시지는 꼭 디테일하게 잘 적어줍시다! 추후 리젝사유가 됩니다.

code

import UIKit
import CoreMotion

class HomeViewController: UIViewController {
    let activityManager = CMMotionActivityManager()
    let pedoMeter = CMPedometer()
    var stepData = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        updateStep()
    }

    func updateStep() {
        if CMPedometer.isStepCountingAvailable() {
            self.pedoMeter.queryPedometerData(from: date.startOfDay(), to: date.endOfDay()) { (date, error) in
                if error == nil {
                    if let response = date {
                        DispatchQueue.main.async {
                            self.stepData = Int(truncating: response.numberOfSteps)
                            
                        }
                    }
                }
            }
        }
    }
}

iOS HealthKit 사용해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


HealthKit

걸음수 데이터를 가져오기 위한 방법은 두가지가 있습니다.

  1. Coremotion
  2. HealthKit

그치만 걸음수 데이터를 무엇을 위해, 어떻게 가져올지에 따라서 둘 중 한가지 방법을 선택해야 합니다.

우선 Coremotion은 실시간 걸음 데이터를 제공합니다.
일반적인 만보기 어플을 구현하기 위해서는 보통 coremotion을 사용합니다.

반면 Healthkit은 애플에서 제공해주는 건강앱에 표시되는 모든 건강데이터들을 그대로 가져옵니다.

즉, 실시간으로 걸음수 데이터를 수집할 목적이라면 coremotion을 사용하고 굳이 실시간 데이터가 필요하지않다면 healthkit을 사용해도 괜찮습니다.

이번에는 실시간으로 걸음수 데이터를 가져오는 HealthKit 사용 방법에 대해서 설명해보겠습니다.

Info.plist

우선 걸음수 데이터를 가져오기 위해 유저에게 접근권한요청을 해야합니다.
plist에 다음과 같은 설정을 추가해줍니다.

NSHealthShareUsageDescription = 걸음수 데이터 측정을 위해 데이터 접근 권한이 필요합니다.
NSHealthUpdateUsageDescription = 걸음수 데이터 측정을 위해 데이터 접근 권한이 필요합니다.

이때 저 메시지.. 꼭 주의해서 작성하세요!
저는 나중에 수정해야지.. 하고 당장 개발할때 대충 작성하곤 까먹고.. 그대로 앱 심사를 올렸더니 문구 수정하라는 코멘트와 함께 리젝을 당했거든요..ㅠ흑

첨부터.. 조심조심 할수있는건 하고 지나가는게 좋겠구나! 라는 교휸을 얻었습니다 ㅎ

code

import UIKit
import HealthKit

class StepModel: NSObject {
    var step: Double = 0.0
    var date: Date = Date()
    override init() {
        super.init()
    }
    convenience init(step: Double, date: Date) {
        self.init()
        self.step = step
        self.date = date
    }
}

class HomeViewController: UIViewController {
    let healthStore = HKHealthStore()
    
    let typeToShare = HKObjectType.quantityType(forIdentifier: .stepCount)
    let typeToRead = HKObjectType.quantityType(forIdentifier: .stepCount)
    
    var stepDataList: [String] = []
    var stepList: [StepModel] = [StepModel]()

    override func viewDidLoad() {
        super.viewDidLoad()
        configure()
        retrieveData()
    }

    func configure() {
        print("configure")
        if !HKHealthStore.isHealthDataAvailable() {
            setHealthData()
        } else {
            requestAuthorization()
        }
    }
    
    // 권한 요청 
    func requestAuthorization() {
        print("requestAuthorization")
        self.healthStore.requestAuthorization(toShare: Set([typeToShare!]), read: Set([typeToRead!])) { success, error in
            if error != nil {
                print(error?.localizedDescription as Any)
            } else {
                if success {
                    print("권한 승인 완료")
                } else {
                    print("권한 승인 실패")
                }
            }
        }
    }
    
    func setHealthData() {
        print("retrieveData")
        let calender = Calendar.current
        var interval = DateComponents()
        interval.day = 1
        
        var anchorComponents = calender.dateComponents([.day, .month, .year], from: NSDate() as Date)
        anchorComponents.hour = 0
        let anchorDate = calender.date(from: anchorComponents)
        
        let stepQuery = HKStatisticsCollectionQuery(quantityType: HKObjectType.quantityType(forIdentifier: .stepCount)!, quantitySamplePredicate: nil, options: .cumulativeSum, anchorDate: anchorDate!, intervalComponents: interval as DateComponents)
        stepQuery.initialResultsHandler = { query, results, error in
            let endDate = NSDate()
            // 오늘로부터 30일간의 걸음수 데이터를 가져오도록 진행 
            let startDate = calender.date(byAdding: .day, value: -30, to: endDate as Date, wrappingComponents: false)
            if let myResults = results {
                myResults.enumerateStatistics(from: startDate!, to: endDate as Date) { statistics, stop in
                    if let quantity = statistics.sumQuantity() {
                        let date = statistics.startDate
                        let steps = quantity.doubleValue(for: HKUnit.count())
                        self.stepDataList.append("\(date): \(steps)")
                        let model = StepModel(step: steps, date: date)
                        self.stepList.append(model)
                        DispatchQueue.main.async {

                        }
                    }
                }
            }
        }
        healthStore.execute(stepQuery)
    }

    func saveStepCount(stepValue: Int, date: Date, completion: @escaping (Error?) -> Void) {
        guard let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return }
        let stepCountUnit: HKUnit = HKUnit.count()
        let stepCountQuantity = HKQuantity(unit: stepCountUnit, doubleValue: Double(stepValue))
        
        let stepCountSample = HKQuantitySample(type: stepCountType, quantity: stepCountQuantity, start: date, end: date)
        
        self.healthStore.save(stepCountSample) { (success, error) in
            if let error = error {
                completion(error)
                print("Error Saving Steps count Sample: \(error.localizedDescription)")
            } else {
                completion(nil)
                print("Successfully saves step count sample")
            }
        }
    }
}

iOS searchBar 초성검색 기능 추가해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


UISeachBar 초성검색 기능 추가해보기

검색바에 좀더 다채로움을 주고싶어서.. 그게 뭐가 있을까 고민해보니..
초성검색 기능이 있으면 괜찮곘다 싶어졌습니다.

let hangeul = ["ㄱ","ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ","ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"]

func chosungCheck(word: String) -> String {
    var result = ""
    
    for char in word {
        // unicodeScalars: 유니코드 스칼라 값의 모음으로 표현되는 문자열 값
        let octal = char.unicodeScalars[char.unicodeScalars.startIndex].value
        // ~=: 왼쪽에서 정의한 범위 값 안에 오른쪽의 값이 속하면 true, 아니면 false 반환
        if 44032...55203 ~= octal {
            let index = (octal - 0xac00) / 28 / 21
            result = result + hangeul[Int(index)]
        }
    }
    return result
}

func isChosung(word: String) -> Bool {
    var isChosung = false
    for char in word {
        if 0 < hangeul.filter({ $0.contains(char)}).count {
            isChosung = true
        } else {
            isChosung = false
            break
        }
    }
    return isChosung
}

그럼 이 코드를 서치바에서 어떻게 적용시킬까요?

var arr = ["zehye", "hi", "hello", "nice", "to", "meet", "you"]
var filterredArr: [String] = []

extension SearchViewController: UISearchBarDelegate {   
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        // 텍스트의 공백 제거
        let text = searchText.trimmingCharacters(in: .whitespaces)
        
        // 서치바에 적힌 공백이 제거된 글자의 초성을 체크
        let isChosungCheck = isChosung(word: text)
        
        // filterText라는 상수를 받아 arr를 필터링 합니다.
        // 서치바에서 받은 text에 arr의 초성이 포함되어있는지 여부를 따집니다. 
        let filterText = arr.filter({
            if isChosungCheck {
                return ($0.contains(text) || chosungCheck(word: $0).contains(text))
            } else {
                return $0.contains(text)
            }
        })
        
        // 포함되는 filterText를 filterredArr에 담아주고 
        self.filterredArr = filterText
        // 테이블뷰 리로드를 합니다. 
        self.tableView.reloadData()
    }
    
    // 검색버튼을 눌렀을때도 마찬가지의 로직을 진행합니다. 
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        
        guard let text = self.searchBar.text?.trimmingCharacters(in: .whitespaces) else { return }
        
        let isChosungCheck = isChosung(word: text)
        
        let filterText = list.filter({
            if isChosungCheck {
                return ($0.contains(text) || chosungCheck(word: $0).contains(text))
            } else {
                return $0.contains(text)
            }
        })
        
        self.filterredArr = filterText
        self.isFiltering = !(self.searchBar.text?.count == 0)
        
        self.tableView.reloadData()
    }
}