본문 바로가기

iOS

iOS_week10

지난 시간 복습

전체 View 밑에 Safe Area 는 디자인을 할 수 있는 공간

 

object 추가 단축키:  shift cmd L

attribute inspector를 사용하여 속정 지정

- background 속성은 View에서 지정

 

connections inspector, 전체를 확인하기 위해서 View Controller 선택

 


 Do it! 스위프트로 아이폰 앱 만들기 입문 

송호정이범근 저 이지스퍼블리싱 2025년 03월.  아래 사이트의 소스는 Xcode 버전 문제가 있어서 첨부 파일로 실습

02 Hello World 앱 만들며 Xcode에 완벽 적응하기 : 2장은 이전 수업시간에 했던 기초 내용의 반복임.

03 원하는 이미지 화면에 출력하기 - 이미지 뷰 : 이미지 확대/축소, 이미지 변경

 

- gpt가 해준 2장 코드 설명

//
//  ViewController.swift
//  ImageView
//
//  Created by Ho-Jeong Song on 2021/11/23.
//

import UIKit   // UIKit 프레임워크를 불러옴 (iOS UI 구성요소 사용을 위해 필요)

class ViewController: UIViewController {  // UIViewController를 상속받은 클래스
    // 이미지 확대/축소 상태를 저장할 변수
    var isZoom = false  // false면 기본 크기, true면 확대 상태
    
    // 이미지 리소스를 저장할 변수
    var imgOn: UIImage?   // 전구 켜짐 이미지
    var imgOff: UIImage?  // 전구 꺼짐 이미지

    // 스토리보드에 연결된 아웃렛(IBOutlet)
    @IBOutlet var imgView: UIImageView!   // 화면에 전구 이미지를 보여주는 이미지뷰
    @IBOutlet var btnResize: UIButton!    // 확대/축소를 위한 버튼
    
    // 뷰가 메모리에 로드된 직후 호출되는 함수 (앱 실행 시 1회 호출)
    override func viewDidLoad() {
        super.viewDidLoad()
        // 추가 초기 설정을 수행하는 부분
        
        // 이미지 리소스를 불러옴 (Assets.xcassets 또는 프로젝트에 포함된 이미지 파일)
        imgOn = UIImage(named: "lamp_on.png")
        imgOff = UIImage(named: "lamp_off.png")
        
        // 앱 실행 시 기본으로 전구 켜짐 이미지를 표시
        imgView.image = imgOn
    }

    // "확대/축소" 버튼이 눌렸을 때 호출되는 액션 함수
    @IBAction func btnResizeImage(_ sender: UIButton) {
        let scale: CGFloat = 2.0  // 확대/축소 비율
        var newWidth: CGFloat, newHeight: CGFloat  // 변경된 크기를 담을 변수

        if (isZoom) {   // 현재 확대 상태일 때 (true)
            // 이미지를 원래 크기로 축소
            newWidth = imgView.frame.width / scale
            newHeight = imgView.frame.height / scale
            
            // 버튼의 제목을 "확대"로 변경 (다음 동작이 확대가 됨을 의미)
            btnResize.setTitle("확대", for: .normal)
        }
        else {  // 현재 축소 상태일 때 (false)
            // 이미지를 2배로 확대
            newWidth = imgView.frame.width * scale
            newHeight = imgView.frame.height * scale
            
            // 버튼의 제목을 "축소"로 변경 (다음 동작이 축소가 됨을 의미)
            btnResize.setTitle("축소", for: .normal)
        }
        
        // 이미지뷰의 크기를 새 크기로 설정 (이미지 자체가 아닌 프레임을 조정)
        imgView.frame.size = CGSize(width: newWidth, height: newHeight)
        
        // 상태를 반전시킴 (true <-> false)
        isZoom = !isZoom
    }
    
    // 스위치가 켜짐/꺼짐으로 바뀔 때 호출되는 액션 함수
    @IBAction func switchImageOnOff(_ sender: UISwitch) {
        if sender.isOn {
            // 스위치가 켜진 경우: 전구 켜진 이미지로 변경
            imgView.image = imgOn
        } else {
            // 스위치가 꺼진 경우: 전구 꺼진 이미지로 변경
            imgView.image = imgOff
        }
    }
}

 

- refactoring code

//
//  ViewController.swift
//  ImageView
//
//  Created by Ho-Jeong Song on 2021/11/23.
//  Refactored by ChatGPT on 2025/11/03.
//

import UIKit

final class ViewController: UIViewController {
    
    // MARK: - Properties
    private var isZoomed = false
    private let scaleFactor: CGFloat = 2.0
    
    private var lampOnImage: UIImage?
    private var lampOffImage: UIImage?
    
    // MARK: - Outlets
    @IBOutlet private weak var imageView: UIImageView!
    @IBOutlet private weak var resizeButton: UIButton!
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupImages()
        setupInitialView()
    }
    
    // MARK: - Setup Methods
    private func setupImages() {
        lampOnImage = UIImage(named: "lamp_on.png")
        lampOffImage = UIImage(named: "lamp_off.png")
    }
    
    private func setupInitialView() {
        imageView.image = lampOnImage
        resizeButton.setTitle("확대", for: .normal)
    }
    
    // MARK: - Actions
    @IBAction private func didTapResizeButton(_ sender: UIButton) {
        toggleImageSize()
    }
    
    @IBAction private func didChangeSwitchState(_ sender: UISwitch) {
        imageView.image = sender.isOn ? lampOnImage : lampOffImage
    }
    
    // MARK: - Helper Methods
    private func toggleImageSize() {
        let newScale: CGFloat = isZoomed ? 1 / scaleFactor : scaleFactor
        let newWidth = imageView.frame.width * newScale
        let newHeight = imageView.frame.height * newScale
        
        imageView.frame.size = CGSize(width: newWidth, height: newHeight)
        resizeButton.setTitle(isZoomed ? "확대" : "축소", for: .normal)
        isZoomed.toggle()
    }
}

개선 항목	설명
✅ final class 사용	상속이 필요 없는 ViewController는 final로 선언하여 성능 최적화
✅ private 접근 제어자 사용	외부에서 접근할 필요 없는 속성/메서드는 private으로 보호
✅ MARK 주석 구분	Properties, Lifecycle, Actions 등을 구분해 구조를 명확히 함
✅ isZoom → isZoomed 변수명 변경	Swift naming convention: 상태를 나타낼 때 과거분사 사용 (isEnabled, isHidden 등)
✅ setupImages() / setupInitialView() 분리	초기 설정 코드를 역할별로 분리해 가독성 향상
✅ 삼항 연산자 사용 (sender.isOn ? ... : ...)	조건문 단순화
✅ toggle() 메서드 사용	불리언 값 반전 시 isZoomed = !isZoomed → isZoomed.toggle() 로 간결하게 변경
✅ 매직 넘버 제거	scaleFactor 상수로 정의하여 의미를 명확히 함

 

04 데이트 피커 사용해 날짜 선택하기 : UIDatePicker(날짜 선택기) 와 UILabel(현재 시간 표시) 를 이용해,
현재 시간과 사용자가 선택한 시간을 화면에 표시하는 iOS 앱

//
//  ViewController.swift
//  DatePicker
//
//  Created by Ho-Jeong Song on 2021/11/24.
//

import UIKit   // UIKit 프레임워크를 불러옴 (UI 요소를 사용하기 위해 필요)

class ViewController: UIViewController {
    
    // MARK: - Properties
    
    // 타이머가 호출할 함수(메서드)를 지정하기 위한 셀렉터(Selector)
    let timeSelector: Selector = #selector(ViewController.updateTime)
    
    // 타이머가 작동하는 간격 (초 단위) — 1초마다 반복
    let interval = 1.0
    
    // 카운터 (테스트용 변수로, 코드에서 현재는 사용되지 않음)
    var count = 0

    // MARK: - Outlets
    
    // 현재 시간을 표시할 UILabel (Storyboard와 연결)
    @IBOutlet var lblCurrentTime: UILabel!
    
    // 사용자가 DatePicker에서 선택한 시간을 표시할 UILabel
    @IBOutlet var lblPickerTime: UILabel!
    
    
    // MARK: - Lifecycle
    
    // 뷰가 메모리에 로드된 직후 한 번만 호출됨
    override func viewDidLoad() {
        super.viewDidLoad()
        // 화면이 처음 로드될 때 수행되는 초기 설정 코드
        
        // 타이머 설정:
        // 1초(interval)마다 timeSelector에 지정된 함수(updateTime)를 호출함.
        Timer.scheduledTimer(
            timeInterval: interval,  // 반복 간격: 1초
            target: self,            // 타이머가 호출할 대상: 현재 뷰 컨트롤러
            selector: timeSelector,  // 호출할 메서드: updateTime()
            userInfo: nil,           // 추가 정보 전달 없음
            repeats: true            // 반복 실행 여부: true → 계속 반복
        )
    }

    
    // MARK: - Actions
    
    // 사용자가 DatePicker에서 날짜/시간을 변경할 때 호출되는 함수
    @IBAction func changeDatePicker(_ sender: UIDatePicker) {
        let datePickerView = sender  // 선택된 UIDatePicker 인스턴스 가져오기
        
        // 날짜와 시간을 보기 좋게 문자열로 바꾸기 위한 포맷터
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm EEE"  
        // 예시 결과: "2025-11-03 14:25 Mon"
        
        // 선택된 날짜를 포맷에 맞게 문자열로 변환하여 라벨에 표시
        lblPickerTime.text = "선택시간: " + formatter.string(from: datePickerView.date)
    }

    
    // MARK: - Timer Callback
    
    // 타이머에 의해 1초마다 호출되는 함수
    @objc func updateTime() {
        // 아래 주석 처리된 코드는 count 변수를 단순히 증가시키는 테스트용 코드였음
        // lblCurrentTime.text = String(count)
        // count = count + 1

        // 현재 시각(Date) 객체를 생성
        let date = NSDate()
        
        // 날짜를 문자열로 보기 좋게 변환하기 위한 DateFormatter 생성
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss EEE"
        // 예시 결과: "2025-11-03 14:25:43 Mon"
        
        // 현재 시간을 문자열로 변환하여 라벨에 표시
        lblCurrentTime.text = "현재시간: " + formatter.string(from: date as Date)
    }
}

 

05 피커 뷰 사용해 원하는 항목 선택하기: UIPickerView(픽커뷰) 를 사용해서 여러 이미지 중 하나를 선택하고,
그 이미지 이름과 실제 이미지를 화면에 표시하는 iOS 예제

//
//  ViewController.swift
//  PickerView
//
//  Created by Ho-Jeong Song on 2021/11/25.
//

import UIKit   // UIKit 프레임워크 (UI 구성요소 사용을 위해 필요)


// UIViewController를 상속받고, UIPickerView의 Delegate & DataSource 프로토콜을 채택
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
    
    // MARK: - Constants
    
    // 사용할 이미지의 개수 (배열 크기)
    let MAX_ARRAY_NUM = 10
    
    // PickerView의 열(column) 수 — 현재는 1열짜리 픽커뷰
    let PICKER_VIEW_COLUMN = 1
    
    // PickerView 각 행(row)의 높이 (픽커 안에서 이미지 표시 높이)
    let PICKER_VIEW_HEIGHT: CGFloat = 80
    
    
    // MARK: - Variables
    
    // 실제 UIImage 객체들을 저장할 배열
    var imageArray = [UIImage?]()
    
    // 이미지 파일 이름들을 저장하는 배열
    var imageFileName = [
        "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg",
        "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg"
    ]

    
    // MARK: - Outlets (Storyboard 연결 요소)
    
    // 이미지 선택을 위한 UIPickerView
    @IBOutlet var pickerImage: UIPickerView!
    
    // 선택된 이미지 파일 이름을 표시할 UILabel
    @IBOutlet var lblImageFileName: UILabel!
    
    // 선택된 이미지를 크게 보여줄 UIImageView
    @IBOutlet var imageView: UIImageView!
    
    
    // MARK: - Lifecycle
    
    // 뷰가 메모리에 로드된 직후 한 번 실행됨
    override func viewDidLoad() {
        super.viewDidLoad()
        // 추가 초기 설정 코드
        
        // 이미지 파일 이름 배열을 순회하며 UIImage 객체를 생성해 imageArray에 추가
        for i in 0 ..< MAX_ARRAY_NUM {
            let image = UIImage(named: imageFileName[i])
            imageArray.append(image)
        }
        
        // 앱 실행 시 초기화: 첫 번째 이미지 파일명과 이미지를 표시
        lblImageFileName.text = imageFileName[0]
        imageView.image = imageArray[0]
    }

    
    // MARK: - UIPickerViewDataSource (필수 구현 메서드)
    
    // PickerView의 열(column) 개수를 반환
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return PICKER_VIEW_COLUMN
    }
    
    // 각 행(row)의 높이를 설정
    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return PICKER_VIEW_HEIGHT
    }

    // PickerView의 각 열에 표시될 행(row)의 개수를 반환
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return imageFileName.count
    }
    
    
    // MARK: - UIPickerViewDelegate (행의 내용 설정)
    
    /*
    // 텍스트로 행을 표시하고 싶을 때 사용하는 메서드 (현재는 주석 처리됨)
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return imageFileName[row]
    }
    */

    // 각 행(row)에 표시할 "뷰(View)"를 반환하는 메서드
    // 이 경우, 이미지뷰(UIImageView)를 만들어 반환함
    func pickerView(
        _ pickerView: UIPickerView,
        viewForRow row: Int,
        forComponent component: Int,
        reusing view: UIView?
    ) -> UIView {
        // 해당 행에 맞는 이미지를 이용해 UIImageView 생성
        let imageView = UIImageView(image: imageArray[row])
        
        // 이미지뷰의 크기를 지정 (픽커뷰 내부에 표시될 크기)
        imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 150)
        
        // 완성된 UIImageView를 반환 → PickerView가 행에 표시함
        return imageView
    }

    
    // 사용자가 PickerView의 특정 행(row)을 선택했을 때 호출되는 메서드
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // 선택된 파일 이름을 레이블에 표시
        lblImageFileName.text = imageFileName[row]
        
        // 선택된 이미지를 메인 이미지뷰에 표시
        imageView.image = imageArray[row]
    }
}

 

06 얼럿 사용해 경고 표시하기 : UIAlertController(알림창) 을 이용해서
램프를 켜기 / 끄기 / 제거하기 기능을 구현한 예제

//
//  ViewController.swift
//  Alert
//
//  Created by BeomGeun Lee on ...
//

import UIKit   // iOS UI를 구성하는 UIKit 프레임워크 불러오기

class ViewController: UIViewController {
    
    // MARK: - 이미지 리소스 정의
    // 전구 상태에 따라 표시할 세 가지 이미지 로드
    let imgOn = UIImage(named: "lamp-on.png")        // 전구 켜짐 이미지
    let imgOff = UIImage(named: "lamp-off.png")      // 전구 꺼짐 이미지
    let imgRemove = UIImage(named: "lamp-remove.png")// 전구 제거된 이미지
    
    // 현재 램프가 켜져 있는지 여부를 저장하는 변수
    var isLampOn = true
    
    // 스토리보드와 연결된 UIImageView (전구 이미지 표시용)
    @IBOutlet var lampImg: UIImageView!
    
    
    // MARK: - 생명주기 메서드
    override func viewDidLoad() {
        super.viewDidLoad()
        // 화면이 처음 로드될 때 실행됨
        // 앱 시작 시 기본 상태: 램프를 켜진 이미지로 설정
        lampImg.image = imgOn
    }

    
    // MARK: - "램프 켜기" 버튼 액션
    @IBAction func btnLampOn(_ sender: UIButton) {
        // 만약 현재 램프가 이미 켜져 있다면
        if isLampOn == true {
            // UIAlertController 생성 (경고창 스타일)
            let lampOnAlert = UIAlertController(
                title: "경고", 
                message: "현재 On 상태입니다", 
                preferredStyle: .alert
            )
            
            // 확인 버튼 추가 (단순히 창만 닫음)
            let onAction = UIAlertAction(
                title: "네, 알겠습니다.", 
                style: .default, 
                handler: nil
            )
            
            // 버튼을 경고창에 추가
            lampOnAlert.addAction(onAction)
            
            // 경고창 표시
            present(lampOnAlert, animated: true, completion: nil)
        } else {
            // 램프가 꺼져 있던 경우 → 켬
            lampImg.image = imgOn
            isLampOn = true
        }
    }
    
    
    // MARK: - "램프 끄기" 버튼 액션
    @IBAction func btnLanpOff(_ sender: UIButton) {
        // 현재 램프가 켜져 있을 때만 동작
        if isLampOn {
            // 끌지 여부를 묻는 AlertController 생성
            let lampOffAlert = UIAlertController(
                title: "램프 끄기", 
                message: "램프를 끄시겠습니까?", 
                preferredStyle: .alert
            )

            // "네" 버튼 → 램프를 끄고 상태 변경
            let offAction = UIAlertAction(
                title: "네", 
                style: .default,
                handler: { ACTION in
                    self.lampImg.image = self.imgOff
                    self.isLampOn = false
                }
            )
            
            // "아니오" 버튼 → 아무 동작 안 함 (Alert만 닫힘)
            let cancelAction = UIAlertAction(
                title: "아니오", 
                style: .default, 
                handler: nil
            )

            // 두 버튼을 Alert에 추가
            lampOffAlert.addAction(offAction)
            lampOffAlert.addAction(cancelAction)

            // Alert 표시
            present(lampOffAlert, animated: true, completion: nil)
        }
    }
    
    
    // MARK: - "램프 제거" 버튼 액션
    @IBAction func btnLampRemove(_ sender: UIButton) {
        // 램프 제거 여부를 묻는 알림창 생성
        let lampRemoveAlert = UIAlertController(
            title: "램프 제거", 
            message: "램프를 제거하시겠습니까?", 
            preferredStyle: .alert
        )

        // 첫 번째 버튼: "아니오, 끕니다(off)."
        // → 램프를 꺼진 상태로 변경
        let offAction = UIAlertAction(
            title: "아니오, 끕니다(off).", 
            style: .default,
            handler: { ACTION in
                self.lampImg.image = self.imgOff
                self.isLampOn = false
            }
        )

        // 두 번째 버튼: "아니오, 켭니다(on)."
        // → 램프를 켜진 상태로 변경
        let onAction = UIAlertAction(
            title: "아니오, 켭니다(on).", 
            style: .default,
            handler: { ACTION in
                self.lampImg.image = self.imgOn
                self.isLampOn = true
            }
        )

        // 세 번째 버튼: "네, 제거합니다."
        // → 램프 이미지를 제거된 상태로 변경
        // destructive 스타일: 빨간색 글씨로 표시됨 (주의를 주는 의미)
        let removeAction = UIAlertAction(
            title: "네, 제거합니다.", 
            style: .destructive,
            handler: { ACTION in
                self.lampImg.image = self.imgRemove
                self.isLampOn = false
            }
        )

        // 생성한 버튼 3개를 Alert에 추가
        lampRemoveAlert.addAction(offAction)
        lampRemoveAlert.addAction(onAction)
        lampRemoveAlert.addAction(removeAction)

        // Alert 표시
        present(lampRemoveAlert, animated: true, completion: nil)
    }
}

 

07 웹 뷰로 간단한 웹 브라우저 만들기

 

 

08 맵 뷰로 지도 나타내기: MapKitCoreLocation을 이용해서
📍현재 위치를 지도에 표시하고,
📍버튼(세그먼티드 컨트롤)을 눌러 다른 위치(예: 인덕대학교, 이지스퍼블리싱)를 보여주는 iOS 앱입니다.

//
//  ViewController.swift
//  Map
//
//  Created by BeomGeun Lee on 2021
//

import UIKit
import MapKit   // 지도 기능 제공 (MKMapView, MKAnnotation 등)
import CoreLocation // 위치 정보 관련 클래스(CLLocationManager 등) 사용

// CLLocationManagerDelegate: 위치 정보를 업데이트 받을 때 사용
class ViewController: UIViewController, CLLocationManagerDelegate {

    // MARK: - IBOutlet 연결 (스토리보드 UI 요소)
    @IBOutlet var myMap: MKMapView!              // 지도 화면 표시용
    @IBOutlet var lblLocationInfo1: UILabel!     // 위치 설명 1줄 (예: "현재 위치" 등)
    @IBOutlet var lblLocationInfo2: UILabel!     // 위치 설명 2줄 (주소 등)
    
    // 위치 관리를 담당할 매니저 객체 생성
    let locationManager = CLLocationManager()
    
    // MARK: - 뷰 로드 시 호출
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 초기 라벨 내용 비우기
        lblLocationInfo1.text = ""
        lblLocationInfo2.text = ""
        
        // 위치 매니저 설정
        locationManager.delegate = self                         // 델리게이트 연결
        locationManager.desiredAccuracy = kCLLocationAccuracyBest // 최고 정확도로 위치 측정
        locationManager.requestWhenInUseAuthorization()           // 앱 실행 중 위치 접근 허용 요청 (권한 알림 표시)
        locationManager.startUpdatingLocation()                   // 위치 정보 업데이트 시작
        myMap.showsUserLocation = true                            // 지도에 파란 점(현재 위치) 표시
    }
    
    
    // MARK: - 지도 이동 함수
    /// 특정 위도/경도 좌표와 확대 범위를 기준으로 지도 위치 이동
    func goLocation(latitudeValue: CLLocationDegrees, longitudeValue : CLLocationDegrees, delta span :Double) -> CLLocationCoordinate2D {
        
        // 위도, 경도를 CLLocationCoordinate2D 타입으로 생성
        let pLocation = CLLocationCoordinate2DMake(latitudeValue, longitudeValue)
        
        // 지도 확대/축소 범위(span): delta 값이 작을수록 더 확대됨
        let spanValue = MKCoordinateSpan(latitudeDelta: span, longitudeDelta: span)
        
        // 지도에 표시할 영역(region) 생성 (중심 좌표 + 확대 범위)
        let pRegion = MKCoordinateRegion(center: pLocation, span: spanValue)
        
        // 지도를 해당 지역으로 이동 (애니메이션 효과 포함)
        myMap.setRegion(pRegion, animated: true)
        
        // 좌표 리턴 (annotation 등에서 사용 가능)
        return pLocation
    }

    
    // MARK: - 지도에 핀(마커) 표시 함수
    func setAnnotation(latitudeValue: CLLocationDegrees,
                       longitudeValue: CLLocationDegrees,
                       delta span: Double,
                       title strTitle: String,
                       subtitle strSubtitle: String) {
        
        // 핀(annotation) 객체 생성
        let annotation = MKPointAnnotation()
        
        // 지정된 위치로 지도 이동 후 해당 좌표를 annotation에 설정
        annotation.coordinate = goLocation(latitudeValue: latitudeValue,
                                           longitudeValue: longitudeValue,
                                           delta: span)
        
        // 핀 제목과 부제목 설정
        annotation.title = strTitle
        annotation.subtitle = strSubtitle
        
        // 지도에 핀 추가
        myMap.addAnnotation(annotation)
    }
    
    
    // MARK: - CLLocationManagerDelegate 메서드 (위치 업데이트 시 호출)
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        
        // 배열의 마지막 위치(가장 최근 위치) 가져오기
        let pLocation = locations.last
        
        // 해당 위치로 지도 이동 (줌: delta 0.01 → 매우 가까이 확대)
        _ = goLocation(latitudeValue: (pLocation?.coordinate.latitude)!,
                       longitudeValue: (pLocation?.coordinate.longitude)!,
                       delta: 0.01)
        
        // 역지오코딩(좌표 → 주소 변환)
        CLGeocoder().reverseGeocodeLocation(pLocation!, completionHandler: {
            (placemarks, error) -> Void in
            
            // placemarks 배열 중 첫 번째 정보 사용
            let pm = placemarks!.first
            
            // 국가 이름 가져오기
            let country = pm!.country
            var address: String = country!
            
            // 도시(locality)가 존재할 경우 주소 문자열에 추가
            if pm!.locality != nil {
                address += " "
                address += pm!.locality!
            }
            
            // 도로명(thoroughfare)이 존재할 경우 주소 문자열에 추가
            if pm!.thoroughfare != nil {
                address += " "
                address += pm!.thoroughfare!
            }
            
            // 화면 라벨에 표시
            self.lblLocationInfo1.text = "현재 위치"
            self.lblLocationInfo2.text = address
        })
        
        // 한 번 위치를 가져온 뒤에는 추가 업데이트 중지 (배터리 절약)
        locationManager.stopUpdatingLocation()
    }

    
    // MARK: - 세그먼티드 컨트롤 (지도 위치 변경)
    @IBAction func sgChangeLocation(_ sender: UISegmentedControl) {
        
        // 첫 번째 세그먼트 (현재 위치)
        if sender.selectedSegmentIndex == 0 {
            self.lblLocationInfo1.text = ""
            self.lblLocationInfo2.text = ""
            // 다시 위치 정보 업데이트 시작
            locationManager.startUpdatingLocation()
        }
        
        // 두 번째 세그먼트 (인덕대학교)
        else if sender.selectedSegmentIndex == 1 {
            setAnnotation(latitudeValue: 37.6314191,
                          longitudeValue: 127.0548249,
                          delta: 1,
                          title: "인덕대학교",
                          subtitle: "서울특별시 노원구 초안산로 12")
            
            self.lblLocationInfo1.text = "보고 계신 위치"
            self.lblLocationInfo2.text = "인덕대학교"
        }
        
        // 세 번째 세그먼트 (이지스퍼블리싱)
        else if sender.selectedSegmentIndex == 2 {
            setAnnotation(latitudeValue: 37.556876,
                          longitudeValue: 126.914066,
                          delta: 0.1,
                          title: "이지스퍼블리싱",
                          subtitle: "서울시 마포구 잔다리로 109 이지스 빌딩")
            
            self.lblLocationInfo1.text = "보고 계신 위치"
            self.lblLocationInfo2.text = "이지스퍼블리싱 출판사"
        }
    }
}

 

09 페이지 이동하기 - 페이지 컨트롤: 이 코드는 UIPageControl을 이용해
📸 여러 이미지를 좌우로 넘기듯 보여주는 간단한 iOS 앱

페이지 컨트롤의 색상 변경

import UIKit

// 전역 변수로 이미지 파일 이름들을 저장한 배열
// -> 총 6장의 이미지를 순서대로 보여줄 예정
var images = [ "01.png", "02.png", "03.png", "04.png", "05.png", "06.png" ]

// MARK: - 메인 뷰 컨트롤러
class ViewController: UIViewController {

    // MARK: - IBOutlet 연결 (스토리보드 UI 요소)
    @IBOutlet var imgView: UIImageView!        // 현재 선택된 이미지를 표시할 이미지 뷰
    @IBOutlet var pageControl: UIPageControl!  // 페이지(인디케이터) 컨트롤

    // MARK: - 뷰가 로드될 때 실행되는 함수
    override func viewDidLoad() {
        super.viewDidLoad()
        // 화면이 처음 로드될 때 실행되는 초기 설정 코드
        
        // PageControl의 총 페이지 개수를 images 배열의 개수와 동일하게 설정
        pageControl.numberOfPages = images.count
        
        // 현재 선택된 페이지를 첫 번째(0번 인덱스)로 지정
        pageControl.currentPage = 0
        
        // 비활성(선택되지 않은) 페이지 인디케이터 색상 설정 (파란색)
        pageControl.pageIndicatorTintColor = UIColor.blue
        
        // 활성(현재 선택된) 페이지 인디케이터 색상 설정 (빨간색)
        pageControl.currentPageIndicatorTintColor = UIColor.red
        
        // 처음 앱을 실행할 때, 첫 번째 이미지를 이미지뷰에 표시
        imgView.image = UIImage(named: images[0])
    }

    // MARK: - PageControl 값이 변경될 때 호출되는 액션 함수
    @IBAction func pageChange(_ sender: UIPageControl) {
        // 현재 PageControl의 선택된 인덱스(currentPage)에 맞는 이미지로 변경
        imgView.image = UIImage(named: images[pageControl.currentPage])
    }
}

 

10 탭 바 컨트롤러 이용해 여러 개의 뷰 넣기 : 가장 많이 사용, bmi 계산기 만들때도 사용

탭바 컨트롤러를 사용하여 여러 화면을 연결

 

11 내비게이션 컨트롤러 이용해 화면 전환하기 : 상세화면을 보여주거나 설정하는 등, 화면을 이동할때 Navigation controller 사용

 

12 테이블 뷰 컨트롤러 이용해 할 일 목록 만들기 : 많이 사용하지만 어려워서 이번학기에 사용 X

 

13 음악 재생하고 녹음하기

 

14 비디오 재생 앱 만들기

//
//  ViewController.swift
//  MoviePlayer
//
//  Created by Ho-Jeong Song on 2021/11/26.
//
import UIKit // 사용자 인터페이스 관련 클래스(View, Button 등)를 사용하기 위해 UIKit 프레임워크를 가져옵니다.
import AVKit // 오디오 및 비디오 콘텐츠 재생을 위한 표준 인터페이스(AVPlayerViewController 등)를 사용하기 위해 AVKit 프레임워크를 가져옵니다.
import AVFoundation // 비디오 재생 자체에 필요한 핵심 기능을 제공하지만, 여기서는 AVKit이 대부분의 작업을 처리합니다. (AVPlayer 사용을 위해 간접적으로 필요)

class ViewController: UIViewController {
    // 뷰 컨트롤러가 메모리에 로드된 후 호출됩니다. 초기 설정을 수행하는 곳입니다.
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    // '내부 영화 재생' 버튼이 눌렸을 때 호출되는 액션 메서드입니다.
    @IBAction func btnPlayInternalMovie(_ sender: UIButton) {
        // 내부 파일 mp4 재생
        
        // 1. 앱 번들(Bundle) 내에 있는 "FastTyping.mp4" 파일의 전체 경로를 찾습니다.
        let filePath:String? = Bundle.main.path(forResource: "FastTyping", ofType: "mp4")
        
        // 2. 찾은 파일 경로(String)를 사용하여 NSURL 객체를 생성합니다. (강제 언래핑 '!'을 사용하고 있습니다. 파일이 확실히 존재해야 합니다.)
        let url = NSURL(fileURLWithPath: filePath!)
        
        // 3. 비디오 재생을 시작하는 전용 함수를 호출합니다.
        playVideo(url: url)
    }
    
    // '외부 영화 재생' 버튼이 눌렸을 때 호출되는 액션 메서드입니다.
    @IBAction func btnPlayerExternalMovie(_ sender: UIButton) {
        // 외부 파일 mp4 재생 (웹 URL을 이용)
        
        // 1. 외부 서버에 있는 비디오 파일의 URL을 사용하여 NSURL 객체를 생성합니다. (강제 언래핑 '!'을 사용하고 있습니다. URL이 유효해야 합니다.)
        let url = NSURL(string: "https://dl.dropboxusercontent.com/s/e38auz050w2mvud/Fireworks.mp4")!
        
        // 2. 비디오 재생을 시작하는 전용 함수를 호출합니다.
        playVideo(url: url)
    }
    
    // 주어진 URL을 사용하여 비디오를 재생하는 핵심 함수입니다.
    private func playVideo(url: NSURL)  {
        // 1. 비디오 재생을 위한 뷰 컨트롤러인 AVPlayerViewController 인스턴스를 생성합니다.
        let playerController = AVPlayerViewController()
        
        // 2. 비디오 URL을 사용하여 비디오 재생을 관리하는 AVPlayer 인스턴스를 생성합니다.
        let player = AVPlayer(url: url as URL)
        
        // 3. AVPlayerViewController의 player 속성에 생성한 AVPlayer를 할당합니다.
        playerController.player = player
        
        // 4. 현재 뷰 컨트롤러(self)에서 AVPlayerViewController를 모달 방식으로 화면에 표시합니다.
        //    completion 클로저 { ... } 내의 코드는 화면 표시 애니메이션이 완료된 후 실행됩니다.
        self.present(playerController, animated: true) {
            // 5. 화면 표시 후, 비디오 재생을 시작합니다.
            player.play()
        }
    }
}


15 카메라와 포토 라이브러리에서 미디어 가져오기 : 사진/비디오 촬영은 시뮬레이터로 불가, mac에 핸드폰 연결해서 해야함


16 코어 그래픽스로 화면에 그림 그리기

17 탭과 터치 사용해 스케치 앱 만들기 - 자주 사용함

//
//  ViewController.swift
//  Sketch
//
//  Created by Ho-Jeong Song on 2021/12/01.
//

import UIKit // 사용자 인터페이스 관련 클래스(View, Touch, Image 등)를 사용하기 위해 UIKit 프레임워크를 가져옵니다.

class ViewController: UIViewController {
    // 스토리보드에서 연결된 이미지 뷰(그림이 그려질 영역)입니다.
    @IBOutlet var imgView: UIImageView!
    
    // 터치 이벤트에서 이전 터치 지점의 좌표를 저장하는 변수입니다.
    var lastPoint: CGPoint!
    // 그릴 선의 두께(폭)를 정의합니다. (CGFloat 타입)
    var lineSize:CGFloat = 2.0
    // 그릴 선의 색상을 정의합니다. (Core Graphics에서 사용하는 CGColor 타입)
    var lineColor = UIColor.red.cgColor

    // 뷰 컨트롤러가 메모리에 로드된 후 호출됩니다.
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    // '지우기' 버튼이 눌렸을 때 호출되는 액션 메서드입니다.
    @IBAction func btnClearImageView(_ sender: UIButton) {
        // imgView의 image 속성을 nil로 설정하여 화면을 비웁니다.
        imgView.image = nil
    }
    
    // 사용자가 화면을 터치하기 시작했을 때 호출됩니다.
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // 발생한 터치 이벤트 중 첫 번째 터치 객체를 가져옵니다.
        let touch = touches.first! as UITouch
        
        // 터치된 지점의 좌표를 imgView 내에서 계산하여 lastPoint에 저장합니다.
        lastPoint = touch.location(in: imgView)
    }
    
    // 사용자가 화면을 터치한 채로 움직일 때(드래그) 계속해서 호출됩니다.
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        // 1. 새로운 그래픽 컨텍스트(Context)를 생성하고 imgView의 크기로 설정합니다.
        UIGraphicsBeginImageContext(imgView.frame.size)
        
        // 2. 현재 그래픽 컨텍스트에 그리기 설정을 합니다.
        UIGraphicsGetCurrentContext()?.setStrokeColor(lineColor) // 선 색상 설정
        UIGraphicsGetCurrentContext()?.setLineCap(CGLineCap.round) // 선 끝 모양을 둥글게 설정
        UIGraphicsGetCurrentContext()?.setLineWidth(lineSize) // 선 두께 설정
        
        // 3. 현재 터치된 지점의 좌표를 가져옵니다.
        let touch = touches.first! as UITouch
        let currPoint = touch.location(in: imgView) // 현재 지점
        
        // 4. 이전에 그려진 그림(imgView.image)을 새로 만든 컨텍스트에 그대로 그립니다. (덧그리기)
        imgView.image?.draw(in: CGRect(x: 0, y: 0, width: imgView.frame.size.width, height: imgView.frame.size.height))
        
        // 5. 이전 지점(lastPoint)에서 현재 지점(currPoint)까지 선을 그립니다.
        UIGraphicsGetCurrentContext()?.move(to: CGPoint(x: lastPoint.x, y: lastPoint.y)) // 선 그리기 시작점 설정
        UIGraphicsGetCurrentContext()?.addLine(to: CGPoint(x: currPoint.x, y: currPoint.y)) // 현재 지점까지 선 추가
        UIGraphicsGetCurrentContext()?.strokePath() // 실제로 선을 그립니다.
        
        // 6. 현재 컨텍스트에 그려진 내용을 UIImage로 변환하여 imgView의 새로운 이미지로 설정합니다.
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        // 7. 현재 그래픽 컨텍스트를 닫고 해제합니다.
        UIGraphicsEndImageContext()
        
        // 8. 현재 지점을 다음 움직임의 '이전 지점'으로 갱신합니다.
        lastPoint = currPoint
    }
    
    // 사용자가 화면에서 손가락을 떼었을 때 호출됩니다.
    // touchesEnded의 로직은 touchesMoved와 거의 동일하나,
    // lastPoint에서 lastPoint로 '점'을 찍어 터치가 끝날 때 발생하는 미세한 딜레이/오차를 보정하고
    // 터치 시작과 끝이 같은 지점일 때(점 찍기) 그림이 그려지도록 보장하는 역할도 합니다.
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        // 1. 새로운 그래픽 컨텍스트를 생성합니다.
        UIGraphicsBeginImageContext(imgView.frame.size)
        
        // 2. 그리기 설정을 합니다.
        UIGraphicsGetCurrentContext()?.setStrokeColor(lineColor)
        UIGraphicsGetCurrentContext()?.setLineCap(CGLineCap.round)
        UIGraphicsGetCurrentContext()?.setLineWidth(lineSize)
        
        // 3. 이전에 그려진 그림을 컨텍스트에 덧그립니다.
        imgView.image?.draw(in: CGRect(x: 0, y: 0, width: imgView.frame.size.width, height: imgView.frame.size.height))
        
        // 4. 마지막 지점에 점을 찍어줍니다. (lastPoint -> lastPoint)
        UIGraphicsGetCurrentContext()?.move(to: CGPoint(x: lastPoint.x, y: lastPoint.y))
        UIGraphicsGetCurrentContext()?.addLine(to: CGPoint(x: lastPoint.x, y: lastPoint.y))
        UIGraphicsGetCurrentContext()?.strokePath()
        
        // 5. 새로운 이미지를 imgView에 할당하고 컨텍스트를 닫습니다.
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
    
    // 사용자가 기기를 흔들었을 때(쉐이크 제스처) 호출됩니다.
    override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        // 흔들림 동작(.motionShake)이 감지되면
        if motion == .motionShake {
            // 그림을 지웁니다.
            imgView.image = nil
        }
    }
}

미션 프로젝트

18 스와이프 제스처 사용하기 : UISwipeGestureRecognizer 사용

19 핀치 제스처 사용해 사진을 확대/축소하기

 


BMI 판정 앱

Apple App Store에서 찾아본 bmi 앱

 

 

1️⃣ 정의

 

  • format: 형식 문자열(format string)
    • C 언어나 Objective-C에서 쓰던 printf 스타일
    • %d, %f, %@ 등 자리 표시자를 사용
  • arguments: 가변 인자(variable arguments)
    • format 문자열 안의 자리 표시자에 대응되는 값들을 순서대로 전달

2️⃣ 동작 원리

String(format:_:)은:

  1. format 문자열에서 %로 시작하는 플레이스홀더를 찾음
  2. arguments에서 순서대로 값을 가져와 그 위치에 치환
  3. 결과를 새로운 String 객체로 만들어 반환

즉, 템플릿 + 값 → 최종 문자열 과정을 담당합니다.

 

-클래스로 만든 BMI 판정 코드

// bmiMode.swift
import Foundation
class BMI {
    var weight : Double
    var height : Double
    init(weight:Double, height:Double){
        self.height = height
        self.weight = weight
    }
    func calcBMI() -> String {
        let bmi=weight/(height*height*0.0001)// kg/m*m
        let shortenedBmi = String(format: "%.1f", bmi)
        var body = ""
        if bmi >= 40 {
            body = "3단계 비만"
        } else if bmi >= 30 && bmi < 40 {
            body = "2단계 비만"
        } else if bmi >= 25 && bmi < 30 {
            body = "1단계 비만"
        } else if bmi >= 18.5 && bmi < 25 {
            body = "정상"
        } else {
            body = "저체중"
        }
        return "BMI:\(shortenedBmi), 판정:\(body)"
    }
}
var yoo = BMI(weight:71.5, height:171.0)
print(yoo.calcBMI())

 

 

Storyboard  vs SwiftUI

개념 시각적 UI 편집기에서 화면을 구성하고 ViewController와 연결 선언형(Declarative) 방식으로 코드로 UI를 구성
개발 방식 드래그 앤 드롭, Interface Builder 사용 코드 기반, 구조체와 뷰를 선언
학습 난이도 초보자 친화적, 시각적으로 바로 UI 확인 가능 처음에는 코드 중심이라 진입 장벽 있음
실시간 미리보기 제한적, 시뮬레이터/디바이스에서 확인 필요 Canvas에서 실시간 미리보기 가능
재사용성 ViewController 단위 재사용은 가능하지만 코드 재사용은 제한적 View 단위 재사용 가능, 모듈화 쉬움
버전 관리(Git) XML 파일 형태라 충돌 발생 시 해결 어려움 코드 기반이라 충돌 해결이 비교적 용이
호환성 iOS 9 이상 iOS 13 이상
레이아웃 Auto Layout, Storyboard Constraints 사용 SwiftUI의 레이아웃 시스템(HStack, VStack, ZStack 등) 사용
애니메이션 제한적, 직접 코드 작성 필요 선언형 애니메이션 지원, 간단히 구현 가능
프로토타입 제작 빠르게 화면 구성 가능 코드 작성 필요, 초기 구현 속도 느릴 수 있음
유지보수 화면이 많아지면 Storyboard가 커져 관리 어려움 작은 View 단위로 쪼개기 쉬워 유지보수 용이

 

Text Field의 keyboard Type : Decimal Pad - 숫자만 입력 가능

bmi 앱 만들기. 다음시간에 계속

'iOS' 카테고리의 다른 글

iOS_week13  (1) 2025.11.24
iOS_week12  (0) 2025.11.17
iOS_week9  (0) 2025.10.27
iOS_week6  (0) 2025.10.13
iOS_week5  (0) 2025.09.29