본문 바로가기
아이폰 개발/Swift

swift 프로퍼티 리스트 - feat: pickerView & tableView

by 인생여희 2021. 3. 26.

swift 프로퍼티 리스트 - feat:  pickerView & tableView

 

 

완성화면

 

 

스토리보드

 

 

 

설명

 

[1] 프로퍼티 리스트

 

 프로퍼티 리스트는 App Bundle 영역에 설치됩니다.

 프로퍼티 리스트는 XML 포맷으로 Key-Value 형식으로 저장하는 데이터 리스트입니다.

 확장자는 .plist 입니다.

 프로퍼티 리스트는 앱의 설정 정보, 환경 설정 데이터 저장 등을 기록하는 데 사용됩니다.

 프로퍼티 리스트는 딕셔너리 방식으로 저장되기 때문에

 키를 가지고 저장 데이터를 가져올 수 있습니다.

 만약 중복된 키를 사용하면 기존 데이터는 지워지고 새로운 데이터가 덮어써지게 됩니다.

 하나의 키에 배열이나, 딕셔너리 타입의 데이터를 넣을 수도 있습니다.

 프로퍼티 리스트는 데이터 타입을 추상화하여 저장하기 때문에 연관된 어느 타입으로든 읽을 수 있습니다.

 

 

 

[2] UserDefaults

 

 기본 저장소인 UserDefaults는 프로퍼티 리스트 기반으로 하여 .plist 파일을 생성하여 데이터를 저장합니다.

 앱 실행 과정 중 데이터가 단순한 구조인 경우 UserDefaults에 저장하는 것이 적절합니다.

 UserDefaults는 런타임에 동작하는 객체입니다.

 싱글톤 패턴이라 앱 전체에 하나의 인스턴스만 생성됩니다.

 이 인스턴스는 앱 전체가 공유할 수 있습니다.

 UserDefaults 객체는 한 객체가 값을 읽고 다른 객체가 값을 변경하게 되는 데서 오는 동시성 문제에 대해 안전하게 설계되어 있습니다.

 먼저 들어온 요청에 우선권을 부여하여 잠금을 걸고 작업이 끝나면 잠금이 해제되어 다음 요청을 받습니다.

 

 UserDefaults 객체로 반환되는 값은 옵셔널 타입입니다.

 1. 키에 해당하는 데이터가 없을 때

 2. 값이 nil로 저장되어 있을 때

 3. 타입 캐스팅이 실패했을 때

 nil이 반환됩니다.

 

 

 

[3] pickerView 프로토타입 채팅

 

 피커 뷰는 콤보 박스와 비슷한 기능을 하는 뷰입니다.

 피커 뷰는 델리게이트 패턴으로 동작하며 테이블 뷰와 유사합니다.

 피커 뷰는 텍스트 필드와 함께 사용하면 inputView 영역을 이용해서 가상 키보드 대신 피커 뷰를 띄울 수 있습니다.

 

 

 

 

순서 - 함수

 

피커뷰 생성 + 피커뷰 델리게이트 설정

툴바 설정

피커뷰 선택 완료 함수

피커뷰 계정 추가 함수

테이블 뷰 행 개수

행 클릭시 Alert 띄우기

성별 함수 남자 - 0 , 여자 - 1

메소드 결혼여부 이벤트 함수

피커뷰 델리게이트 메소드

 

 

 

소스코드

class ViewController: UITableViewController , UIPickerViewDelegate , UIPickerViewDataSource{

    

    //이름
    @IBOutlet weak var name: UILabel!
    
    //성별
    @IBOutlet weak var gender: UISegmentedControl!
    
    //결혼여부
    @IBOutlet weak var married: UISwitch!
    
    //계정
    @IBOutlet weak var account: UITextField!
    
    //피커뷰 내용 배열
    var accountList = [String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        //UserDefaults 싱글톤 객체
        let plist = UserDefaults.standard
        
        //저장된 값을 꺼내어 각 컨트롤러에 적용
        self.name.text = plist.string(forKey: "name")
        self.married.isOn = plist.bool(forKey: "married")
        self.gender.selectedSegmentIndex = plist.integer(forKey: "gender")
        
        //MARK:-피커뷰 생성 + 피커뷰 델리게이트 설정
        let picker = UIPickerView()
        picker.delegate = self
        
        //텍스트 필드 입력 방식을 키보드 대신 피커뷰로 설정
        self.account.inputView = picker
        
        
        
        //MARK:- 툴바
        let toolbar = UIToolbar()
        toolbar.frame = CGRect(x: 0, y: 0, width: 0, height: 35)
        toolbar.barTintColor = .lightGray
        
        //악세사리 뷰 영역에 툴 바 표시
        self.account.inputAccessoryView = toolbar
        
        //확인 버튼
        let done = UIBarButtonItem()
        done.title = "Done"
        done.target = self
        done.action = #selector(pickerDone)
        
        
        //가변폭 버튼
        let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        
        
        //신규 계정 추가 버튼
        let newAcc = UIBarButtonItem()
        newAcc.title = "New"
        newAcc.target = self
        newAcc.action = #selector(newAccount)
        
        
        //버튼을 툴바에 추가
        toolbar.setItems([newAcc ,flexSpace, done], animated: true)
        
        //계정 배열 셋팅
        let accountlistFromUserdefault = plist.array(forKey: "accountlist") as? [String] ?? [String]()
        
        self.accountList = accountlistFromUserdefault
        
        if let account = plist.string(forKey: "selectedAccount"){
            self.account.text = account
            

        }
        
    }

   //MARK:-피커뷰 선택 완료 함수
    @objc func pickerDone(_ sender: Any){
        self.view.endEditing(true)
    }
    
    
    //MARK:- 피커뷰 계정 추가 함수
    @objc func newAccount(_ sender: Any){
        self.view.endEditing(true)
        
        //팝업창
        let alert = UIAlertController(title: "새 계정을 입력하세요", message: nil, preferredStyle: .alert)
        
        alert.addTextField(configurationHandler: {
            $0.placeholder = "ex)avvv@naver.com"
        })
        
        //계정 저장
        alert.addAction(UIAlertAction(title: "ok", style: .default, handler: { (_) in
            
            if let account = alert.textFields?[0].text{
                
                //배열에 추가
                self.accountList.append(account)
                self.account.text = account
                
                
                //컨트롤 값 초기화
                self.name.text = ""
                self.gender.selectedSegmentIndex = 0
                self.married.isOn = false
                
                //계정 목록을 저장
                let plist = UserDefaults.standard
                plist.setValue(self.accountList, forKey: "accountlist")
                plist.setValue(account, forKey: "selectedAccount")
                plist.synchronize()
                
                
            }
            
        }))//addAction
        
        //띄우기
        self.present(alert, animated: false, completion: nil)
        
    }
    
    //MARK:- 테이블 뷰 행 개수
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    /*
         위의 메서드는 테이블 뷰 행의 개수를 반환하는 메서드입니다.
         지금 3개의 행이 사용되고 있어서 return 3으로 하드 코딩해도 상관없지만
         부모 클래스에 정의된 상위 메서드를 호출하면 셀의 개수를 자동으로 반환하도록 할 수 있습니다.

         오버라이드 메서드에서 부모 메서드가 처리하는 기능을 그대로 쓰고 싶다면
         직접 구현하기 보다 메서드를 아예 삭제해버리면 됩니다.

         자식 메서드에서 메서드가 구현되어 있지 않으면 동일한 부모 메서드가 호출되기 때문입니다.
         그래서 지금 코드에서는 tableView 메서드 관련은 모두 삭제하겠습니다.
         */
        
        return super.tableView(tableView, numberOfRowsInSection: section)
    }

    //MARK:- 행 클릭시 Alert 띄우기
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        if indexPath.row == 0 {
            let alert = UIAlertController(title: nil, message: "이름을 입력하세요", preferredStyle: .alert)
            
            
            alert.addTextField(configurationHandler: {
                //name 키로 값을 저장
                $0.text = self.name.text
            })
            
            
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
                //ok 버튼 핸들러
                
                //이름
                let value = alert.textFields?[0].text
                
                //저장 로직
        /*
                 참고
                 커스텀 프로퍼티 파일을 다루려면 설치된 애플리케이션 영역 내부에 지정되는 샌드박스 공간을 이용합니다.
                 이 샌드박스 공간에만 파일을 읽고 쓸 수 있습니다.
                 샌드박스 안에는 디렉터리가 정의되어 있는데 이 디렉터리의 경로는 아래의 구문으로 읽어올 수 있습니다.
                 NSSearchPathForDirectoriesInDomains 객체는 해당 앱과 연관된 디렉터리를 반환합니다.
                 첫 번째 인자의 대상을 두 번째 인자 값의 범위에서 찾아 반환합니다.
                 세 번째 인자는 전체 디렉터리 경로를 반환(true), 디렉터리 명만 반환(false)을 지정합니다.
                 조건에 부합하는 디렉터리가 여러 개일 경우를 위해 디렉터리 경로는 배열 형식입니다.
            */
                let customPlist = "\(self.account.text!).plist" //커스텀 프로퍼티 파일명
                let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
                let path = paths[0] as NSString //결과로 나오는 경로는 Documents 디렉터리
                let plist = path.strings(byAppendingPaths: [customPlist]).first! //URL
                //완성된 경로를 가지고 딕셔너리에 담습니다.
                let data = NSMutableDictionary(contentsOfFile: plist) ?? NSMutableDictionary()
                data.setValue(value, forKey: "name")
                data.write(toFile: plist, atomically: true)
                
//                let plist = UserDefaults.standard
//                plist.set(value, forKey: "name")
//                plist.synchronize() //동기화
                
                //수정된 값을 레이블에 적용
                self.name.text = value
                
            }))
            
            self.present(alert, animated: false, completion: nil)
        }
    }
    
    
/*
     selectedSegmentIndex는 인덱스 (0부터 시작)으로 접근할 수 있습니다.
     synchronize()는 데이터 저장 후 반드시 캐싱 된 데이터를 갱신해서 데이터를 일치시키는 작업을 합니다.
     데이터 저장 후에는 반드시 synchronize()를 호출해서 동기화 처리를 해주는 것이 좋습니다.
*/
    //MARK:- 성별 함수 남자 - 0 , 여자 - 1
    @IBAction func changeGender(_ sender: UISegmentedControl) {
        
        let value = sender.selectedSegmentIndex // 0 남자, 1 여자
        
        let customPlist = "\(self.account.text!).plist" //커스텀 프로퍼티 파일명
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let path = paths[0] as NSString //결과로 나오는 경로는 Documents 디렉터리
        let plist = path.strings(byAppendingPaths: [customPlist]).first! //URL
        //완성된 경로를 가지고 딕셔너리에 담습니다.
        let data = NSMutableDictionary(contentsOfFile: plist) ?? NSMutableDictionary()
        data.setValue(value, forKey: "gender")
        data.write(toFile: plist, atomically: true)
        
        print("custom plist \(plist)")
        
//        //저장소 객체를 가져옴
//        let plist = UserDefaults.standard
//        //key value 저장
//        plist.set(value, forKey: "gender")
//        //동기화 처리
//        plist.synchronize()
        
    }
    
    

    //MARK:-  메소드 결혼여부 이벤트 함수
    @IBAction func changeMarried(_ sender: UISwitch) {
        let value = sender.isOn
        
        
        let customPlist = "\(self.account.text!).plist" //커스텀 프로퍼티 파일명
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let path = paths[0] as NSString //결과로 나오는 경로는 Documents 디렉터리
        let plist = path.strings(byAppendingPaths: [customPlist]).first! //URL
        //완성된 경로를 가지고 딕셔너리에 담습니다.
        let data = NSMutableDictionary(contentsOfFile: plist) ?? NSMutableDictionary()
        data.setValue(value, forKey: "married")
        data.write(toFile: plist, atomically: true)
        
        print("custom plist \(plist)")
        
//        let plist = UserDefaults.standard
//        plist.set(value, forKey: "married")
//        plist.synchronize()
        
        
    }
    
    
    //MARK:- 피커뷰 델리게이트 메소드
    
    //생성할 컴포넌트의 개수 정의
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    //컴포넌트가 가질 목록의 길이
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return self.accountList.count
    }
    
    
    //컴포넌트의 목록 각 행에 출력될 내용
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        
        return self.accountList[row]
    }
    
    //컴포넌트의 행을 선택했을 때 실행할 액션
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        
        //선탁한 계정
        let account = self.accountList[row]
        self.account.text = account
        
        //선택한 계정 저장
        let plist = UserDefaults.standard
        plist.set(account, forKey: "selectedAccount")
        plist.synchronize()
        
        //입력뷰 닫기
        //self.view.endEditing(true)
        
    }
}

 

 

PropertyList.zip
0.04MB

 

참고

https://blog.naver.com/go4693/221381217759