[Swift] Xcode 주요 UI 오픈 소스 분석

승민·2024년 11월 17일
0

iOS

목록 보기
9/12
post-thumbnail

알아야 할 점

  • 본 문서는 iOS 프로그래밍 강의 내용을 중심으로 작성되었어요.
  • 이전 내용을 모르면 이해가 어려울 수 있어요.
  • 앞으로도 계속 내용을 추가할 예정이에요.

목적

  • 다른 iOS 앱의 코드를 분석할 예정이에요.
  • 다양한 기능을 알아보고 더 활용할 방법을 알아볼거에요.

학습 순서

코드 분석앱 디자인 및 키보드 설정

1. 코드 분석

오픈소스로 있는 코드의 실행화면과 코드를 확인해볼거에요.

1 - 1. Page Control : 페이지 이동하기 - 페이지 컨트롤

import UIKit

// 이미지 파일 이름들을 담은 배열
var images = [ "01.png", "02.png", "03.png", "04.png", "05.png", "06.png" ]

class ViewController: UIViewController {
    
    // 이미지 뷰와 페이지 컨트롤을 IBOutlet으로 연결
    @IBOutlet var imgView: UIImageView!
    @IBOutlet var pageControl: UIPageControl!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 뷰가 로드될 때 실행되는 코드
        
        // 페이지 컨트롤에 표시할 페이지 수를 이미지 배열의 개수로 설정
        pageControl.numberOfPages = images.count
        // 현재 페이지를 3으로 설정 (처음에는 3번 페이지로 설정)
        pageControl.currentPage = 3
        
        // 페이지 컨트롤의 비활성 페이지 인디케이터 색상을 회색으로 설정
        pageControl.pageIndicatorTintColor = UIColor.gray
        // 현재 페이지 인디케이터 색상을 검은색으로 설정
        pageControl.currentPageIndicatorTintColor = UIColor.black
        
        // 첫 번째 이미지(0번 인덱스)를 이미지 뷰에 표시
        imgView.image = UIImage(named: images[0])
    }

    // 페이지 컨트롤의 페이지가 변경되었을 때 호출되는 액션 메소드
    @IBAction func pageChange(_ sender: UIPageControl) {
        // 페이지 컨트롤의 현재 페이지에 해당하는 이미지를 이미지 뷰에 표시
        imgView.image = UIImage(named: images[pageControl.currentPage])
    }
    
}

1 - 2. Tab Bar : 탭 바 컨트롤러 이용해 여러 개의 뷰 넣기

1 - 3. Navigation : 내비게이션 컨트롤러 이용해 화면 전환하기

import UIKit

// ViewController는 UIViewController를 상속받고, EditDelegate 프로토콜을 채택함
class ViewController: UIViewController, EditDelegate {

    // 조명 켜짐/꺼짐 상태에 따라 사용할 이미지 파일을 선언
    let imgOn = UIImage(named: "lamp_on.png")  // 조명이 켜진 상태 이미지
    let imgOff = UIImage(named: "lamp_off.png")  // 조명이 꺼진 상태 이미지
    
    var isOn = true  // 조명의 상태 (기본적으로 켜짐 상태로 설정)

    // IBOutlet을 사용하여 스토리보드에서 UI 요소에 연결
    @IBOutlet var txMessage: UITextField!  // 텍스트 입력 필드 (메시지 입력용)
    @IBOutlet var imgView: UIImageView!  // 이미지 뷰 (조명 이미지 표시용)
    
    // 화면이 로드될 때 호출되는 메서드
    override func viewDidLoad() {
        super.viewDidLoad()
        // 추가적인 초기화 작업이 있을 경우 여기에 작성
        imgView.image = imgOn  // 화면에 조명이 켜진 상태의 이미지를 표시
    }

    // segue가 발생하기 전에 호출되는 메서드
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // EditViewController로 segue를 통해 전환될 때 실행되는 코드
        let editViewController = segue.destination as! EditViewController
        
        // segue의 식별자를 확인하여 어떤 방식으로 전환되었는지 구분
        if segue.identifier == "editButton" {
            editViewController.textWayValue = "segue : use button"  // 버튼을 통해 이동한 경우
        } else if segue.identifier == "editBarButton" {
            editViewController.textWayValue = "segue : use Bar button"  // 바 버튼을 통해 이동한 경우
        }
        
        // 현재 ViewController에서 입력된 텍스트와 조명 상태를 EditViewController로 전달
        editViewController.textMessage = txMessage.text!  // 메시지 텍스트
        editViewController.isOn = isOn  // 조명의 켜짐/꺼짐 상태
        editViewController.delegate = self  // EditViewController가 ViewController의 delegate를 참조하도록 설정
    }
    
    // EditViewController에서 메시지가 수정 완료되면 호출되는 delegate 메서드
    func didMessageEditDone(_ controller: EditViewController, message: String) {
        txMessage.text = message  // 전달받은 메시지로 텍스트 필드를 업데이트
    }
    
    // EditViewController에서 조명의 상태(켜짐/꺼짐)가 변경되면 호출되는 delegate 메서드
    func didImageOnOffDone(_ controller: EditViewController, isOn: Bool) {
        if isOn {
            imgView.image = imgOn  // 조명이 켜진 상태로 이미지 변경
            self.isOn = true  // 조명이 켜졌다고 상태 변경
        } else {
            imgView.image = imgOff  // 조명이 꺼진 상태로 이미지 변경
            self.isOn = false  // 조명이 꺼졌다고 상태 변경
        }
    }
}

1 - 4. Table : 테이블 뷰 컨트롤러 이용해 할 일 목록 만들기

import UIKit

// 데이터 항목 목록과 해당 항목에 대응하는 이미지 파일 이름 목록
var items = ["책 구매", "철수와 약속", "스터디 준비하기"]
var itemsImageFile = ["cart.png", "clock.png", "pencil.png"]

class TableViewController: UITableViewController {

    // 테이블 뷰 UI 요소 연결
    @IBOutlet var tvListView: UITableView!
    
    // 뷰가 로드된 후 호출되는 메서드
    override func viewDidLoad() {
        super.viewDidLoad()

        // 내비게이션 바에 '편집' 버튼을 추가
        self.navigationItem.leftBarButtonItem = self.editButtonItem
    }

    // 뷰가 화면에 나타날 때마다 테이블 뷰 데이터를 새로 고침
    override func viewWillAppear(_ animated: Bool) {
        tvListView.reloadData()
    }

    // MARK: - Table view data source

    // 섹션 수를 반환
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1  // 하나의 섹션만 사용
    }

    // 주어진 섹션에 포함된 행의 개수를 반환
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count  // 'items' 배열의 개수만큼 행을 생성
    }

    // 각 행에 표시할 셀을 반환
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 셀을 재사용 큐에서 꺼내서 설정
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath)

        // 셀의 텍스트와 이미지를 설정
        cell.textLabel?.text = items[(indexPath as NSIndexPath).row]  // 항목 텍스트 설정
        cell.imageView?.image = UIImage(named: itemsImageFile[(indexPath as NSIndexPath).row])  // 항목 이미지 설정

        return cell
    }

    // MARK: - Table view editing support

    // 테이블 뷰의 특정 행을 삭제할 수 있도록 설정
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            // 데이터 소스에서 항목과 이미지를 삭제하고, 테이블 뷰에서 해당 행 삭제
            items.remove(at: (indexPath as NSIndexPath).row)
            itemsImageFile.remove(at: (indexPath as NSIndexPath).row)
            tableView.deleteRows(at: [indexPath], with: .fade)
        } else if editingStyle == .insert {
            // 항목 삽입을 위한 로직 (추가 작업 필요)
        }
    }

    // 삭제 버튼의 텍스트를 "삭제"로 설정
    override func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
        return "삭제"
    }

    // MARK: - Rearranging support

    // 테이블 뷰에서 행을 이동할 수 있도록 설정
    override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
        // 이동할 항목과 해당 이미지 가져오기
        let itemToMove = items[(fromIndexPath as NSIndexPath).row]
        let itemImageToMove = itemsImageFile[(fromIndexPath as NSIndexPath).row]
        
        // 원본 위치에서 항목과 이미지 삭제
        items.remove(at: (fromIndexPath as NSIndexPath).row)
        itemsImageFile.remove(at: (fromIndexPath as NSIndexPath).row)
        
        // 새 위치에 항목과 이미지 삽입
        items.insert(itemToMove, at: (to as NSIndexPath).row)
        itemsImageFile.insert(itemImageToMove, at: (to as NSIndexPath).row)
    }

    // MARK: - Navigation

    // 스토리보드에서 segue를 통해 다른 뷰로 전환될 때 데이터를 준비하는 메서드
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "sgDetail" {
            // 선택된 셀을 가져와서, 그 셀에 해당하는 인덱스 경로를 찾음
            let cell = sender as! UITableViewCell
            let indexPath = self.tvListView.indexPath(for: cell)
            
            // 세그웨이를 통해 넘겨받은 DetailViewController로 데이터를 전달
            let detailView = segue.destination as! DetailViewController
            detailView.reciveItem(items[((indexPath! as NSIndexPath).row)])  // 선택된 항목의 텍스트 전달
        }
    }
}

1 - 5. Audio : 음악 재생하고 녹음하기

import UIKit
import AVFoundation

class ViewController: UIViewController, AVAudioPlayerDelegate, AVAudioRecorderDelegate {
    
    // 오디오 플레이어 및 파일 URL 변수
    var audioPlayer: AVAudioPlayer!
    var audioFile: URL!
    
    // 최대 볼륨 값
    let MAX_VOLUME: Float = 10.0
    
    // 진행 시간 타이머
    var progressTimer: Timer!
    
    // 플레이 및 레코드 타이머를 업데이트할 셀렉터 정의
    let timePlayerSelector: Selector = #selector(ViewController.updatePlayTime)
    let timeRecordSelector: Selector = #selector(ViewController.updateRecordTime)
    
    // UI 요소 연결
    @IBOutlet var pvProgressPlay: UIProgressView!  // 재생 진행 표시
    @IBOutlet var lblCurrentTime: UILabel!         // 현재 재생 시간
    @IBOutlet var lblEndTime: UILabel!             // 전체 길이
    @IBOutlet var btnPlay: UIButton!               // 플레이 버튼
    @IBOutlet var btnPause: UIButton!              // 일시 정지 버튼
    @IBOutlet var btnStop: UIButton!               // 정지 버튼
    @IBOutlet var slVolume: UISlider!              // 볼륨 슬라이더
    
    @IBOutlet var btnRecord: UIButton!             // 녹음 버튼
    @IBOutlet var lblRecordTime: UILabel!          // 녹음 시간 표시
    
    // 오디오 녹음 관련 변수
    var audioRecorder: AVAudioRecorder!
    var isRecordMode = false  // 녹음 모드 여부
    
    // 뷰가 로드되었을 때 호출되는 메서드
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 오디오 파일을 선택
        selectAudioFile()
        
        // 녹음 모드가 아닌 경우: 오디오 플레이어 초기화 및 녹음 관련 UI 비활성화
        if !isRecordMode {
            initPlay()
            btnRecord.isEnabled = false
            lblRecordTime.isEnabled = false
        } else {
            // 녹음 모드인 경우: 녹음 초기화
            initRecord()
        }
    }
    
    // 오디오 파일을 선택하는 메서드
    func selectAudioFile() {
        // 녹음 모드가 아니면 "Sicilian_Breeze.mp3" 파일을 사용
        if !isRecordMode {
            audioFile = Bundle.main.url(forResource: "Sicilian_Breeze", withExtension: "mp3")
        } else {
            // 녹음 모드이면 문서 디렉토리에서 녹음 파일을 사용
            let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            audioFile = documentDirectory.appendingPathComponent("recordFile.m4a")
        }
    }
    
    // 녹음 초기화 설정
    func initRecord() {
        // 녹음 설정
        let recordSettings = [
            AVFormatIDKey : NSNumber(value: kAudioFormatAppleLossless as UInt32),
            AVEncoderAudioQualityKey : AVAudioQuality.max.rawValue,
            AVEncoderBitRateKey : 320000,
            AVNumberOfChannelsKey : 2,
            AVSampleRateKey : 44100.0
        ] as [String : Any]
        
        do {
            // AVAudioRecorder 객체 생성
            audioRecorder = try AVAudioRecorder(url: audioFile, settings: recordSettings)
        } catch let error as NSError {
            print("Error-initRecord : \(error)")
        }
        
        // 녹음기 델리게이트 설정
        audioRecorder.delegate = self
        
        // 볼륨 슬라이더 초기화 및 오디오 플레이어 볼륨 설정
        slVolume.value = 1.0
        audioPlayer.volume = slVolume.value
        
        // 현재 시간 및 끝 시간을 초기화
        lblEndTime.text = convertNSTimeInterval2String(0)
        lblCurrentTime.text = convertNSTimeInterval2String(0)
        
        // 플레이 관련 버튼 비활성화
        setPlayButtons(false, pause: false, stop: false)
        
        // 오디오 세션 설정 (녹음 및 재생 모드)
        let session = AVAudioSession.sharedInstance()
        do {
            try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch let error as NSError {
            print("Error-setCategory : \(error)")
        }
    }
    
    // 오디오 플레이어 초기화 설정
    func initPlay() {
        do {
            // AVAudioPlayer 객체 생성
            audioPlayer = try AVAudioPlayer(contentsOf: audioFile)
        } catch let error as NSError {
            print("Error-initPlay : \(error)")
        }
        
        // 볼륨 슬라이더 설정
        slVolume.maximumValue = MAX_VOLUME
        slVolume.value = 1.0
        
        // 재생 진행 표시 초기화
        pvProgressPlay.progress = 0
        
        // 오디오 플레이어 준비 및 설정
        audioPlayer.delegate = self
        audioPlayer.prepareToPlay()
        audioPlayer.volume = slVolume.value
        
        // 오디오 파일의 끝 시간을 레이블에 표시
        lblEndTime.text = convertNSTimeInterval2String(audioPlayer.duration)
        lblCurrentTime.text = convertNSTimeInterval2String(0)
        
        // 플레이 버튼 활성화
        setPlayButtons(true, pause: false, stop: false)
    }
    
    // 플레이/일시 정지/정지 버튼 활성화/비활성화 설정
    func setPlayButtons(_ play: Bool, pause: Bool, stop: Bool) {
        btnPlay.isEnabled = play
        btnPause.isEnabled = pause
        btnStop.isEnabled = stop
    }
    
    // NSTimeInterval을 "mm:ss" 형식의 문자열로 변환
    func convertNSTimeInterval2String(_ time: TimeInterval) -> String {
        let min = Int(time / 60)
        let sec = Int(time.truncatingRemainder(dividingBy: 60))
        return String(format: "%02d:%02d", min, sec)
    }

    // 재생 버튼 클릭 시 호출되는 메서드
    @IBAction func btnPlayAudio(_ sender: UIButton) {
        audioPlayer.play()
        setPlayButtons(false, pause: true, stop: true)
        
        // 재생 진행 시간 업데이트 타이머 시작
        progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: timePlayerSelector, userInfo: nil, repeats: true)
    }
    
    // 재생 시간 업데이트 메서드
    @objc func updatePlayTime() {
        lblCurrentTime.text = convertNSTimeInterval2String(audioPlayer.currentTime)
        pvProgressPlay.progress = Float(audioPlayer.currentTime / audioPlayer.duration)
    }
    
    // 일시 정지 버튼 클릭 시 호출되는 메서드
    @IBAction func btnPauseAudio(_ sender: UIButton) {
        audioPlayer.pause()
        setPlayButtons(true, pause: false, stop: true)
    }
    
    // 정지 버튼 클릭 시 호출되는 메서드
    @IBAction func btnStopAudio(_ sender: UIButton) {
        audioPlayer.stop()
        audioPlayer.currentTime = 0
        lblCurrentTime.text = convertNSTimeInterval2String(0)
        setPlayButtons(true, pause: false, stop: false)
        
        // 진행 시간 타이머 중지
        progressTimer.invalidate()
    }
    
    // 볼륨 슬라이더 값 변경 시 호출되는 메서드
    @IBAction func slChangeVolume(_ sender: UISlider) {
        audioPlayer.volume = slVolume.value
    }
    
    // 오디오 재생이 완료되었을 때 호출되는 델리게이트 메서드
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        progressTimer.invalidate()
        setPlayButtons(true, pause: false, stop: false)
    }
    
    // 녹음 모드 스위치 변경 시 호출되는 메서드
    @IBAction func swRecordMode(_ sender: UISwitch) {
        if sender.isOn {
            // 재생 중인 오디오를 정지하고, 녹음 모드로 전환
            audioPlayer.stop()
            audioPlayer.currentTime = 0
            lblRecordTime.text = convertNSTimeInterval2String(0)
            isRecordMode = true
            btnRecord.isEnabled = true
            lblRecordTime.isEnabled = true
        } else {
            // 녹음 모드를 종료하고 재생 모드로 복귀
            isRecordMode = false
            btnRecord.isEnabled = false
            lblRecordTime.isEnabled = false
            lblRecordTime.text = convertNSTimeInterval2String(0)
        }
        
        // 파일 선택 및 초기화
        selectAudioFile()
        
        // 플레이 또는 녹음 초기화
        if !isRecordMode {
            initPlay()
        } else {
            initRecord()
        }
    }
    
    // 녹음/정지 버튼 클릭 시 호출되는 메서드
    @IBAction func btnRecord(_ sender: UIButton) {
        if (sender as AnyObject).titleLabel?.text == "Record" {
            audioRecorder.record()
            (sender as AnyObject).setTitle("Stop", for: .normal)
            
            // 녹음 시간 타이머 시작
            progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: timeRecordSelector, userInfo: nil, repeats: true)
        } else {
            audioRecorder.stop()
            progressTimer.invalidate()
            (sender as AnyObject).setTitle("Record", for: .normal)
            btnPlay.isEnabled = true
            initPlay()
        }
    }
    
    // 녹음 시간 업데이트 메서드
    @objc func updateRecordTime() {
        lblRecordTime.text = convertNSTimeInterval2String(audioRecorder.currentTime)
    }
}

1 - 6. MoviePlayer : 비디오 재생 앱 만들기

import UIKit
import AVKit  // AVKit 프레임워크 임포트 (비디오 재생을 위한 프레임워크)

class ViewController: UIViewController {

    // 뷰가 로드되었을 때 호출되는 메서드
    override func viewDidLoad() {
        super.viewDidLoad()
        // 추가적인 설정이 필요한 경우 여기에 작성
    }

    // "내부 파일 재생" 버튼을 눌렀을 때 호출되는 메서드
    @IBAction func btnPlayInternalMovie(_ sender: UIButton) {
        // 앱 내에 번들에 포함된 "FastTyping.mp4" 파일의 경로를 찾습니다.
        let filePath: String? = Bundle.main.path(forResource: "FastTyping", ofType: "mp4")
        
        // 파일 경로가 유효하면 URL 객체를 생성
        let url = NSURL(fileURLWithPath: filePath!)

        // 비디오 재생 함수 호출 (비디오 URL 전달)
        playVideo(url: url)
    }
    
    // "외부 파일 재생" 버튼을 눌렀을 때 호출되는 메서드
    @IBAction func btnPlayerExternalMovie(_ sender: UIButton) {
        // 외부 URL에서 비디오 파일을 가져옵니다 (Dropbox에서 제공하는 mp4 파일)
        let url = NSURL(string: "https://dl.dropboxusercontent.com/s/e38auz050w2mvud/Fireworks.mp4")!

        // 비디오 재생 함수 호출 (외부 URL 전달)
        playVideo(url: url)
    }
    
    // 비디오 URL을 받아서 AVPlayer로 재생하는 메서드
    private func playVideo(url: NSURL)  {
        // AVPlayerViewController 객체 생성 (비디오를 화면에 표시하는 컨트롤러)
        let playerController = AVPlayerViewController()
        
        // 주어진 URL로 AVPlayer 객체 생성
        let player = AVPlayer(url: url as URL)
        
        // AVPlayerViewController의 player 속성에 AVPlayer 설정
        playerController.player = player
        
        // AVPlayerViewController를 화면에 표시하고 비디오 재생 시작
        self.present(playerController, animated: true) {
            // 비디오 재생 시작
            player.play()
        }
    }
}

1 - 7. CameraPhotoLibrary : 카메라와 포토 라이브러리에서 미디어 가져오기

import UIKit
import MobileCoreServices  // 이미지 및 비디오 관련 미디어 타입을 관리하는 프레임워크

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    @IBOutlet var imgView: UIImageView!  // 이미지 뷰 (선택한 이미지나 비디오 썸네일을 표시할 곳)
    
    let imagePicker: UIImagePickerController! = UIImagePickerController()  // 이미지 피커 인스턴스 생성
    var captureImage: UIImage!  // 캡처된 이미지 저장
    var videoURL: URL!  // 캡처된 비디오 URL 저장
    var flagImageSave = false  // 이미지나 비디오 저장 여부를 결정하는 플래그
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 뷰 로딩 후 추가적인 설정이 필요한 경우 여기서 할 수 있음
    }

    // 카메라에서 이미지를 촬영하는 버튼 액션
    @IBAction func btnCaptureImageFromCamera(_ sender: UIButton) {
        // 카메라가 사용 가능한지 확인
        if (UIImagePickerController.isSourceTypeAvailable(.camera)) {
            flagImageSave = true  // 촬영 후 이미지를 저장할 것인지 결정
            
            imagePicker.delegate = self  // 이미지 피커의 델리게이트를 설정
            imagePicker.sourceType = .camera  // 소스 타입을 카메라로 설정
            imagePicker.mediaTypes = ["public.image"]  // 미디어 타입을 이미지로 설정
            imagePicker.allowsEditing = false  // 이미지 편집 허용 여부 설정
            
            present(imagePicker, animated: true, completion: nil)  // 이미지 피커 화면을 띄운다
        }
        else {
            myAlert("Camera inaccessable", message: "Application cannot access the camera.")  // 카메라 접근 불가 알림
        }
    }
    
    // 라이브러리에서 이미지를 선택하는 버튼 액션
    @IBAction func btnLoadImageFromLibrary(_ sender: UIButton) {
        // 사진 앨범이 사용 가능한지 확인
        if (UIImagePickerController.isSourceTypeAvailable(.photoLibrary)) {
            flagImageSave = false  // 이미지는 저장하지 않겠다고 설정
            
            imagePicker.delegate = self  // 이미지 피커의 델리게이트를 설정
            imagePicker.sourceType = .photoLibrary  // 소스 타입을 사진 앨범으로 설정
            imagePicker.mediaTypes = ["public.image"]  // 미디어 타입을 이미지로 설정
            imagePicker.allowsEditing = true  // 이미지 편집 허용 여부 설정
            
            present(imagePicker, animated: true, completion: nil)  // 이미지 피커 화면을 띄운다
        }
        else {
            myAlert("Photo album inaccessable", message: "Application cannot access the photo album.")  // 사진 앨범 접근 불가 알림
        }
    }
    
    // 카메라에서 비디오를 촬영하는 버튼 액션
    @IBAction func btnRecordVideoFromCamera(_ sender: UIButton) {
        // 카메라가 사용 가능한지 확인
        if (UIImagePickerController.isSourceTypeAvailable(.camera)) {
            flagImageSave = true  // 촬영 후 비디오를 저장할 것인지 결정
            
            imagePicker.delegate = self  // 이미지 피커의 델리게이트를 설정
            imagePicker.sourceType = .camera  // 소스 타입을 카메라로 설정
            imagePicker.mediaTypes = ["public.movie"]  // 미디어 타입을 비디오로 설정
            imagePicker.allowsEditing = false  // 비디오 편집 허용 여부 설정
            
            present(imagePicker, animated: true, completion: nil)  // 이미지 피커 화면을 띄운다
        }
        else {
            myAlert("Camera inaccessable", message: "Application cannot access the camera.")  // 카메라 접근 불가 알림
        }
    }
    
    // 라이브러리에서 비디오를 선택하는 버튼 액션
    @IBAction func btnLoadVideoFromLibrary(_ sender: UIButton) {
        // 사진 앨범이 사용 가능한지 확인
        if (UIImagePickerController.isSourceTypeAvailable(.photoLibrary)) {
            flagImageSave = false  // 비디오는 저장하지 않겠다고 설정
            
            imagePicker.delegate = self  // 이미지 피커의 델리게이트를 설정
            imagePicker.sourceType = .photoLibrary  // 소스 타입을 사진 앨범으로 설정
            imagePicker.mediaTypes = ["public.movie"]  // 미디어 타입을 비디오로 설정
            imagePicker.allowsEditing = false  // 비디오 편집 허용 여부 설정
            
            present(imagePicker, animated: true, completion: nil)  // 이미지 피커 화면을 띄운다
        }
        else {
            myAlert("Photo album inaccessable", message: "Application cannot access the photo album.")  // 사진 앨범 접근 불가 알림
        }
    }
    
    // 미디어 선택 후 처리하는 메서드
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        let mediaType = info[UIImagePickerController.InfoKey.mediaType] as! NSString  // 미디어 타입 확인
        
        // 이미지가 선택된 경우
        if mediaType.isEqual(to: "public.image" as String) {
            captureImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage  // 원본 이미지 저장
            
            // 이미지 저장 여부에 따라 사진 앨범에 저장
            if flagImageSave {
                UIImageWriteToSavedPhotosAlbum(captureImage, self, nil, nil)
            }
            
            imgView.image = captureImage  // 선택된 이미지를 화면에 표시
        }
        // 비디오가 선택된 경우
        else if mediaType.isEqual(to: "public.movie" as String) {
            // 비디오 저장 여부에 따라 비디오 파일을 사진 앨범에 저장
            if flagImageSave {
                videoURL = (info[UIImagePickerController.InfoKey.mediaURL] as! URL)  // 비디오 URL 저장
                UISaveVideoAtPathToSavedPhotosAlbum(videoURL.relativePath, self, nil, nil)
            }
        }

        self.dismiss(animated: true, completion: nil)  // 이미지 피커 화면 닫기
    }
    
    // 취소 버튼을 눌러서 이미지 피커 화면을 닫았을 때 호출되는 메서드
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        self.dismiss(animated: true, completion: nil)  // 이미지 피커 화면 닫기
    }

    // 알림 메시지를 표시하는 메서드
    func myAlert(_ title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
        let action = UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: nil)
        alert.addAction(action)
        self.present(alert, animated: true, completion: nil)  // 알림 표시
    }
}

1 - 8. DrawGraphics : 코어 그래픽스로 화면에 그림 그리기

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet var imgView: UIImageView!  // 이미지 뷰, 도형을 그릴 곳

    override func viewDidLoad() {
        super.viewDidLoad()
        // 뷰가 로드된 후 초기 설정이 필요한 경우 여기서 할 수 있음
    }

    // 직선을 그리는 버튼 액션
    @IBAction func btnDrawLine(_ sender: UIButton) {
        UIGraphicsBeginImageContext(imgView.frame.size)  // 이미지 컨텍스트 시작
        let context = UIGraphicsGetCurrentContext()!  // 현재 그래픽 컨텍스트 얻기
        
        // 빨간색 선을 그리기 위한 설정
        context.setLineWidth(2.0)  // 선의 두께
        context.setStrokeColor(UIColor.red.cgColor)  // 선 색 설정 (빨간색)
        
        // 선의 시작점과 끝점 설정
        context.move(to: CGPoint(x: 70, y: 50))  // 시작점
        context.addLine(to: CGPoint(x: 270, y: 250))  // 끝점
        
        context.strokePath()  // 선 그리기
        
        // 삼각형 그리기
        context.setLineWidth(4.0)  // 선 두께 변경
        context.setStrokeColor(UIColor.blue.cgColor)  // 선 색을 파란색으로 변경
        
        // 삼각형을 만들기 위한 점들 연결
        context.move(to: CGPoint(x: 170, y: 200))
        context.addLine(to: CGPoint(x: 270, y: 350))
        context.addLine(to: CGPoint(x:  70, y: 350))
        context.addLine(to: CGPoint(x: 170, y: 200))
        context.strokePath()  // 삼각형 그리기
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()  // 그린 이미지를 이미지 뷰에 설정
        UIGraphicsEndImageContext()  // 이미지 컨텍스트 종료
    }
    
    // 사각형을 그리는 버튼 액션
    @IBAction func btnDrawRectangle(_ sender: UIButton) {
        UIGraphicsBeginImageContext(imgView.frame.size)
        let context = UIGraphicsGetCurrentContext()!
        
        // 빨간색 선으로 사각형 그리기
        context.setLineWidth(2.0)
        context.setStrokeColor(UIColor.red.cgColor)
        
        // 사각형의 위치와 크기 설정
        context.addRect(CGRect(x: 70, y: 100, width: 200, height: 200))
        context.strokePath()  // 사각형 그리기
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
    
    // 원과 타원을 그리는 버튼 액션
    @IBAction func btnDrawCircle(_ sender: UIButton) {
        UIGraphicsBeginImageContext(imgView.frame.size)
        let context = UIGraphicsGetCurrentContext()!
        
        // 타원 그리기 (빨간색 선)
        context.setLineWidth(2.0)
        context.setStrokeColor(UIColor.red.cgColor)
        
        // 타원의 위치와 크기 설정
        context.addEllipse(in: CGRect(x: 70, y: 50, width: 200, height: 100))
        context.strokePath()  // 타원 그리기
        
        // 원 그리기 (녹색 선)
        context.setLineWidth(5.0)
        context.setStrokeColor(UIColor.green.cgColor)
        
        // 원의 위치와 크기 설정
        context.addEllipse(in: CGRect(x: 70, y: 200, width: 200, height: 200))
        context.strokePath()  // 원 그리기
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
    
    // 호를 그리는 버튼 액션
    @IBAction func btnDrawArc(_ sender: UIButton) {
        UIGraphicsBeginImageContext(imgView.frame.size)
        let context = UIGraphicsGetCurrentContext()!
        
        // 첫 번째 호 그리기 (빨간색 선)
        context.setLineWidth(5.0)
        context.setStrokeColor(UIColor.red.cgColor)
        
        // 호의 시작점, 끝점, 그리고 반지름 설정
        context.move(to: CGPoint(x: 100, y: 50))
        context.addArc(tangent1End: CGPoint(x: 250, y:50), tangent2End: CGPoint(x:250, y:200), radius: CGFloat(50))
        context.addLine(to: CGPoint(x: 250, y: 200))
        
        // 두 번째 호 그리기
        context.move(to: CGPoint(x: 100, y: 250))
        context.addArc(tangent1End: CGPoint(x: 270, y:250), tangent2End: CGPoint(x:100, y:400), radius: CGFloat(20))
        context.addLine(to: CGPoint(x: 100, y: 400))
        
        context.strokePath()  // 호 그리기
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
    
    // 도형을 채우고 그리기 (사각형, 원, 삼각형)
    @IBAction func btnDrawFill(_ sender: UIButton) {
        UIGraphicsBeginImageContext(imgView.frame.size)
        let context = UIGraphicsGetCurrentContext()!
        
        // 빨간색 사각형 그리기 (채우기)
        context.setLineWidth(1.0)
        context.setStrokeColor(UIColor.red.cgColor)
        context.setFillColor(UIColor.red.cgColor)
        
        let rectangel = CGRect(x: 70, y: 50, width: 200, height: 100)
        context.addRect(rectangel)
        context.fill(rectangel)  // 사각형 채우기
        context.strokePath()  // 사각형 외곽선 그리기
        
        // 파란색 원 그리기 (채우기)
        context.setLineWidth(1.0)
        context.setStrokeColor(UIColor.blue.cgColor)
        context.setFillColor(UIColor.blue.cgColor)
        
        let circle = CGRect(x: 70, y: 200, width: 200, height: 100)
        context.addEllipse(in: circle)
        context.fillEllipse(in: circle)  // 원 채우기
        context.strokePath()  // 원 외곽선 그리기
        
        // 초록색 삼각형 그리기 (채우기)
        context.setLineWidth(1.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.setFillColor(UIColor.green.cgColor)
        
        context.move(to: CGPoint(x: 170, y: 350))
        context.addLine(to: CGPoint(x: 270, y: 450))
        context.addLine(to: CGPoint(x:  70, y: 450))
        context.addLine(to: CGPoint(x: 170, y: 350))
        context.fillPath()  // 삼각형 채우기
        context.strokePath()  // 삼각형 외곽선 그리기
        
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
}

1 - 9. DrawGraphics : 탭과 터치 사용해 스케치 앱 만들기

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet var txtMessage: UILabel!  // 터치 이벤트 상태 메시지를 표시할 레이블
    @IBOutlet var txtTapCount: UILabel!  // 터치의 탭 횟수를 표시할 레이블
    @IBOutlet var txtTouchCount: UILabel!  // 동시에 발생한 터치 개수를 표시할 레이블
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 뷰가 로드된 후에 초기 설정을 진행할 수 있는 부분
    }

    // 사용자가 화면을 터치하기 시작했을 때 호출되는 메서드
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch  // 첫 번째 터치 객체를 가져옵니다.
        
        // 터치가 시작되었을 때 상태 메시지와 터치 정보를 레이블에 표시합니다.
        txtMessage.text = "Touches Began"
        txtTapCount.text = String(touch.tapCount)  // 현재 터치의 탭 횟수를 표시합니다.
        txtTouchCount.text = String(touches.count)  // 동시에 발생한 터치 개수를 표시합니다.
    }

    // 사용자가 화면에서 손가락을 움직일 때마다 호출되는 메서드
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch  // 첫 번째 터치 객체를 가져옵니다.
        
        // 터치가 이동할 때 상태 메시지와 터치 정보를 레이블에 표시합니다.
        txtMessage.text = "Touches Moved"
        txtTapCount.text = String(touch.tapCount)  // 현재 터치의 탭 횟수를 표시합니다.
        txtTouchCount.text = String(touches.count)  // 동시에 발생한 터치 개수를 표시합니다.
    }
    
    // 사용자가 화면에서 손가락을 떼었을 때 호출되는 메서드
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch  // 첫 번째 터치 객체를 가져옵니다.
        
        // 터치가 종료될 때 상태 메시지와 터치 정보를 레이블에 표시합니다.
        txtMessage.text = "Touches Ended"
        txtTapCount.text = String(touch.tapCount)  // 현재 터치의 탭 횟수를 표시합니다.
        txtTouchCount.text = String(touches.count)  // 동시에 발생한 터치 개수를 표시합니다.
    }
}

1 - 10. TapTouch : 스와이프 제스처 사용하기

1 - 11. PinchGesture : 핀치 제스처 사용해 사진을 확대/축소하기

추가로 XcodeSimulator에서 핀치 제스처는 Alt 키를 누른 상태에서 드래그하면 사용할 수 있어요.

2. BMI 앱 만들기

지금까지의 내용과 실습 코드를 같이 활용해서 앱을 만들어볼게요.

2 - 1. 기존 앱

앱을 통해 기능을 제공하는 경우 이미 앱스토어에 똑같은 기능을 제공하는 경우가 많아요.
어떤 점을 다르게 만들지 비교하기 위해서 기존에 있는 앱스토어를 확인할 수 있어요.

apps.apple.com

2 - 2. 앱 개발 기본 절차

Xcode에서 앱 개발할 때 기본이 되는 절차가 있어요.

  1. UI디자인
  2. 변수 : Outlet 작성
  3. 함수 : Action 작성
  4. Connections inspector로 연결 확인
  5. Action에서 소스코드 작성

2 - 3. 설정

다음 내용과 같이 설정 후 진행해요.

2 - 4. 디자인

다음과 같이 나타날 수 있도록 앱의 디자인을 해줘요.

2 - 5. 숫자 키보드만 나타나게 하기

입력 시 HeightWeight는 숫자만 입력하게 하는 것이 오류를 방지할 수 있는 방법 중 하나에요.
다음과 같이 Text Field의 속성 값 중 Keyboard TypeDecimal Pad로 설정하면 숫자만 입력 가능한 키보드가 나타나게할 수 있어요.

정리

  • Xcode의 주요 UI를 활용할 수 있는 소스코드를 확인했어요.
  • 앱을 만들 기초 작업을 했어요.
  • 대부분의 기능은 이미 존재하는 경우가 많으니 만들고 싶은 기능이 이미 있는지 확인해보는 것도 좋아요.

출처 : Smile Han - iOS 프로그래밍 기초

오픈소스 출처 : Do it! 스위프트로 아이폰 앱 만들기 입문
송호정, 이범근 저, 이지스퍼블리싱, 2023년 01월 20일

0개의 댓글