콘텐츠로 이동

Swift

1. 소개

2. PlayGround 란?

3. Swift에 대해 알아보자

Safe, Fast, Expressive

4. 상수와 변수

상수: 변하지 않은 일정한 값을 갖는다.
변수: 변할 수 있는 값을 갖는다.

// 상수
// let 상수명: 데이터타입 = 값
let a: Int = 100

// 변수
// var 변수명: 데이터타입 = 값
let b: Int = 200

5. 데이터타입

  • Int : 64bit 정수형
  • UInt: 부호가 없는 64bit 정수형
  • Float: 32bit 부동 소수점
  • Double: 64Bit 부동 소수점
  • Bool: true, false 값
  • Character: 문자
  • String: 문자열
  • Any: 모든 타입을 지칭하는 키워드
// Int
var somInt: Int = -100
somInt = 100

// UInt
var somUInt: UInt = 200

// Float
var someFloat: Float = 1.1
someFloat = 1
print(someFloat) // 1.0 이 출력

// Dobule
var someDouble: Double: 1.1
someDouble = 1

// Bool
var someBool: Bool = true
someBool: false

// Character 
var someCharacter: Character = "가"
someCharacter = "A"
someCharacter = "😃"


// String
var someString: String = "안녕하세요. 😄"

// 타입 추론
var number: 10

6. 컬랙션 타입

컬랙션 타입이란?

컬랙션 타입은 데이터들의 집합 묶음

Array

데이터 타입의 값들을 순서대로 지정하는 리스트

Dictionary

순서없이 키(Key)와 값(Value) 한 쌍으로 데이터를 저장하는 컬랙션 타입

Set

같은 데이터 타입의 값을 순서없이 저장하는 리스트 (중복된 값을 가질수 없음)

// Array
var numbers: Array<Int> = Array<Int>()
numbers.append(1)
numbers.append(2)
numbers.append(3)

numbers[0]
numbers[1]
numbers[2]

numbers.insert(4, at: 2)
numbers

numbers.remove(at: 0)
numbers

var names: [String] = []


// Dictionary
var dic: Dictionary<String, Int> = Dictionary<String, Int>()
var dic: [String: Int] = ["권태완" : 1]

dic["김철수"] = 3
dic["김민지"] = 5
dic

dic["김민지"] = 6
dic

dic.removeValue(forKey: "김민지")
dic

// Set
var set: Set = Set<Int>()

set.insert(10)
set.insert(20)
set.insert(30)
set.insert(30)
set.insert(30)
set

set.remove(20)
set

7. 함수 사용법

함수는 작업의 가장 작은 단위이자 코드의 집합입니다.

함수의 정의와 호출

/*
func 함수명(파라미터 이름: 데이터 타입) -> 반환타입 {
    return 반환겂
}
*/

func sum(a: Int, b: Int) -> Int {
    return a+b
}

sum(a: 5, b: 3)

func Hello() -> String {
    return "hello"
}

hello()

func printName() -> Void {

}

func greeting(friend: String, me: String = "gunter") {
    print("Hello, \(friend)! I'm \(me)")
}

greeting(friend: "Albert")

/*
func 함수 이름(전달인자 레이블: 매개변수 이름: 매개변수 타입, 전달인자 레이블: 매개변수 이름: 매개변수 타입...) -> 반환타입{
    return 반환갑
}
*/

func sendMessage(from myName: String, to name: String) -> String {
    return "Hello \(name)! I'm \(myName)"
}

sendMessag(from: "Gunter", to:"Json") // "Hello Json! I'm Gunter"

// 전달인자 레이블 생략
func SendMessage(_ name: String) -> String {
    return "Hello \(name)"
}

sendMessage("권태완")

// 가변매개변수
func sendMessage(me: String, friends: String...) -> String {
    return "Hello \(friends)! I'm \(me)"
}

sendMessage(me: "Gunter", friends: "Json", "Albert", "Stella")

8. 조건문

주어진 조건에 따라서 애플리케이션을 다르게 동작하도록 하는것이다.

/*
if 조건식 {
    실행할 구문
}
*/
let age = 12

if age < 19  {
    print("미성년자 입니다.")
}

/*
if 조건식 {
    조건식이 만족하면 해당 구문 실행
} else {
    만족하지 않으면 해당 구문 실행
}
*/
if age < 19 {
    print("미성년자")
} else {
    print("성년자")
}


/*
if 조건식 {
    조건식이 만족하면 해당 구문 실행
} else if 조건식 2 {
    조건식 2를만족하면 해당 구문 실행
} else {
    // 아무 조건식도 만족하지 않을 때 실행할 구문
}
*/

let animal = "cat"

if animal == "dog" {
    print("강아지 사료 주기")
} else if animal == "cat" {
    print("고양이 사료 주기")
} else {
        print("해당하는 동물 사료가 없음")
}


/*
switch  비교대상 {
    case 패턴1:
        // 패턴1 일치할 때 실생되는 구문
    case 패턴2, 패턴3:
        // 패턴2, 3이 일치할 때 실행되는 구문
    default:
        // 어느 비교 패턴과도 일히하지 않울 때 실행되는 구문
}
*/


let color = "green"

switch color {
case "blue":
    print("파란색 입니다.")
case "green":
    print("초록색 입니다.")
case "yellow":
    print("노란색 입니다.")
default
    print("착는 색상이 없습니다. ")
}

// 범위연산자 사용
let temperature = 30

switch temperature {
case -20...9:
    print("겨울 입니다.")

case 10...14:
    print("가을 입니다.")

case 15...25:
    print("봄 입니다.")

case 26...35:
    print("여름 입니다.")

default:
    print("이상기후입니다.")
}

9. 반복문

반복적으로 코드가 실행되게 만드는 구문을 말한다.

/*
for 루프상수 in 순회대상 {
// 실행할 구문..
}
*/

for i in 1...4 {
    print(i)
}

let array = [1,2,3,4,5]

for i in array {
    print(i)
}


/*

while 조건식 {
    // 실행할 구문
}
*/

var number = 5

while number < 10 {
    number+=1
}

number

/* 
repat {
// 실행할 구문

} while 조건식
*/

var x = 6 

repeat {
    x+=2
} whle x< 5

print(x) // x = 8
// 조건식에 따라 반복이 실행되지만 적어도 한번은 구문을 실행함

10. 옵셔널

값이 있을수도 없을수도 있음.

var name: String?

var optionalName: String? = "Gunter"

var requiredName: String = optionalName  // 에러발생
  1. 옵셔널 바인딩

  2. 명시적 해제 : 강제 해제, 비강제 해제( 옵셔널 바인딩)

  3. 묵시적 해제 : 컴파일러에 의한 자동 해제, 옵셔널의 묵시적 해제
// 명시적 해제
var number: Int? = 3
print(number)
print(number!) // 강제해제

if let result = number {  // 비강제 해제
    print(result)
} else {
    // nil 일경우
}

func test() {
    let number: Int? = 5
    guard let result = number else { return }
    print(result)
}

test()

// 비교연산자를 비교하면 자동적으로 옵셔널을 해제
let valute = Int? = 6

if value == 6 {
    print("value가 6입니다.")
} else {
    print("value가 6이 아닙니다. ")
}

let string = "12"
var stringToInt: Int! = Int(string) // 묵시적 옵셔널 해제 

12. 구조체

구조체의 정의

/*
struct 구조체 이름 {
    프로퍼티와 메서드
}
*/

struct User {
    var nickname: String
    var age: Int

    func information() {
        print("\(nickname) \(age)")
    }
}

var user = User(nickname: "gunter", age: 23)
user.nickname
user.nickname = "ablert"
user.nickname

user.information()

13. 클래스

클래스의 정의

/*
class 클래스 이름 {
    프로퍼티와 메서드
}
*/

class Dog {
    var name: String = ""
    var age: Int = 0

    init() {
    }

    func introduce() {
        print("name \(name) age \(age)")
}

var dog = Dog()
dog.name = "Coco"
dog.age = 3
dog.name
dog.age 

dog.introduce()

14. 초기화 구문 init

초기화(Initailization)란?

클래스 구조체 또는 열거형의 인스턴스를 사용하기 위한 준비과정

/*
init (매개변수: 타입, ...) {
    // 프로퍼티 초기화
    // 인스턴스 생성시 필요한 설정을 해주는 코드 작성
}
*/

class User {

    var nickname: String
    var age: Int

    init(nickname: String, age: Int) {
        self.nickname = nickname
        self.age = age
    }

    init(age: Int) {
        self.nickanme = "ablert"
        self.age = age
    }

    deinit {
        print("deinit User")
    }
}

var user = User(nickname: "gunter", age: 23)
user.nickname
user.age

var user2 = User(age: 23)
user2.nickname
user2.age

var user3 = User(age: 23)
user3 = nil // deinit User 출력됨

15. 프로퍼티

프로퍼티 종류

  • 저장 프로퍼티
  • 연산 프로퍼티
  • 타입 프로터피
struct Dog {

    var name: String
    var gender: String
} 

var dog = Dog(name: "gunter", gender: "Male") // 저장프로퍼티
print(dog)

dog.name = "권태원"


let dog2 = Dog(name: "gunter", gender: "male")
// struct 는 Value 타입이기때문에 상수로 선언하게되면 변수로 선언된 프로퍼티라 하더라도 변경이 안된다.

class Cat {

    var name: String
    let gender: String

    init (name: String, gender: String) {
        self.name = name
        self.gender = gender
    }
}

let cat = Cat(name: "json", gender: "male")
cat.name = "gunter"
print(cat.name)
// class 는 참조타입이기 때문에 변수로 선언된 프로퍼티의 값이 변경됨

// 계산 프로퍼티
struct Stock {
    var averagePrice: Int
    var quantity: int
    var purchasePrice: Int {
        get {
            return averagePrice * quantity
        } 
        set(newPrice) {
            averagePrice = newPrice / quantity
        }
    }
}

var stock = Stock(averagePrice: 2300, quantity: 3)
print(stock)
stock.purchasePrice
stock.purchasePrice = 3000
stock.purchasePrice


// 저장 프로퍼티
class Account {
    var credit: Int = 0 {
        // 소괄호 이름 지정
        willSet {
            print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정입니다.")
        }
        didSet {
            print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었습니다.")
        }
    }
}

var account = Account()
account.credit = 1000


// 타입프로퍼티
struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}

SomeStructure.computedTypeProperty
SomeStructure.storedtypeProperty
SomeStructure.storedtypeProperty = "hello"
SomeStructure.storedtypeProperty

16. 클래스와 구조체의 차이

공통점

  • 값을 저장할 프로퍼티를 선언할 수 있습니다.
  • 함수적 기능을 하는 메서드를 선언 할 수 있습니다.
  • 내부 값에 . 을 사용하여 접근할 수 있습니다.
  • 생성자를 사용해 초기 상태를 설정할 수 있습니다.
  • extension을 사용하여 기능을 확장할 수 있습니다.
  • Protocol을 채택하여 기능을 설정할 수 있습니다.

차이점

클래스 구조체
참조타입 값타입
ARC로 메모리를 관리 구조체 변수를 새로운 변수에 할당할 때마다 새로운 구조체가 할당됩니다.
상속이 가능 즉 같은 구조체를 여러 개의 변수에 할당한 뒤 값을 변경시키더라도 다른 변수에 영향을 주지 않음 (값 자체를 복사)
타입 캐스팅을 통해 런타임에서 클래스 인스턴스의 타입을 확인할 수 있음
deinit을 사용하여 클래스 인스턴스의 메모리 할당을 해제할 수 있음
같은 클래스 인스턴스를 여러 개의 변수에 할당한 뒤 값을 변경 시키면 모든 변수에 영향을 줌 (메모리가 복사 됨)
class SomeClass {
    var count: Int = 0

}

struct SomeSturct {
    var count: Int =  0
}

var class1 = SomeClass()
var class2 = class1
var class3 = class1

class3.count = 2
class1.count  // 2 
// 변수를 복사하더라도 하나의 주소를 가리키기 때문은 값이 같게됨

var struct1 = SomeStruct()
var struct2 = struct1
var struct3 = struct1

struct2.count = 3
struct3.count = 4

struct1.count  // 0
struct2.count  // 3
struct3.count  // 4 
// 매번 새로운 메모리에 할당되기때문에

17. 상속

클래스가 다른 클래로부터 메소드, 프로퍼티 또는 다른 특성들을 상속받는것을 말한다.

class Vehicle {
    var currentSpped = 0.0
    var descrpition: String {
        return "traveling at \(currentSpeed) miles per hour"
    }
    func makeNoise() {
        print("speaker on")
    }
}

/* 
class 클래스이름: 부모클래스이름 {
    // 하위 클래스 정의 
}
*/

class Bicycle: Vehicle {
    var hasBasket = false
}

var bicycle = Bicycle()
bicycle.currentSpped
bicycle.currentSpped = 15.0
bicycle.currentSpped


// 오버라이드
class Train: Vehicle {

override func makeNoise() {
    super.makeNoise()
    print("choo choo")
}

let train = Train()
train.makeNoise()


// 프로퍼티 오버라이드
class Car: Vehicle {
    var gear = 1
    override var description: String {
        retrun super.description = " in gear \(gear)"
    }
}

let car = Car()
car.currentSpeed = 30.0
car.gear = 2
print(car.description)


class AutomaticCar: Car {
    override var currentSpped: Double {
        didSet {
            gear = Int(currentSpped / 10) + 1
        }
    }
}

let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
print("AutomaticCar: \(automatic.description)")  // AutomaticCar: traveling at 35.0 miles per hour in gear 4

18. 타입캐스팅

인스턴스의 타입을 확인하거나 어떠한 클래스의 인스턴스를

해당 클래스 계층 구조의 슈퍼 클래스나 서브 클래스로 취급 하는 방법

is, as 연산자를 사용

is 연산자 사용

class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name:name)
    }
}

let library = [
    Movie(name: "기생충", director: "봉준호"),
    Song(name: "Butter", artist: "BTS"),
    Movie(name: "올드보이", director: "박찬욱"),
    Song(name: "Wonderwall", artist: "Oasis"),
    Song(name: "Rain", artist: "이적")
]

var moveCount = 0
var songCount = 0

for item in library {
    if item is Movie {
        moveCount += 1
    } else if item is Song {
        songCount += 1
    }
}

print("Media library contains \(moveCount) move and \(songCount) songs")

as 연산자 사용

// 위 for문을 아래와 같이 변경 
for item in library {
    if let move = item as? Movie {
                print("Move: \(movie.name), dir. \(movie.director)"
    } else if let song = item as? Song {
                print("Song: \(song.name), by \(movie.artist)"
    }
}

19. assert 와 guard

assert

  • 특정 조건을 체크하고, 조건이 성립되지 않으면 메세지를 출력하게 할 수 있는 함수
  • assert 함수는 디버깅 모드에서만 동작하고 주로 디버깅 중 조건의 검증을 위하여 사용합니다.

guard

  • 뭔가를 검사하여 그 다음에 오는 코드를 실행할지 말지 결정하는 것
  • guard문에 주어진 조건문이 거짓일 때 구문이 실행됨

assert

var value = 0

assert(value == 0)


value = 2

assert(value == 0, "값이 0이 아닙니다.")

실행시 아래와 같은 에러 발생

__lldb_expr_3/MyPlayground.playground:11: Assertion failed: 값이 0이 아닙니다.
Playground execution failed:

error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x18f2e9a10).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x18f2e9a10)
  * frame #0: 0x000000018f2e9a10 libswiftCore.dylib`Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 300
    frame #1: 0x0000000100d18534 $__lldb_expr4`main at MyPlayground.playground:0
    frame #2: 0x000000010097f4f0 MyPlayground`linkResources + 264
    frame #3: 0x000000018034ff90 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 20
    frame #4: 0x000000018034f254 CoreFoundation`__CFRunLoopDoBlocks + 408
    frame #5: 0x0000000180349b10 CoreFoundation`__CFRunLoopRun + 764
    frame #6: 0x0000000180349308 CoreFoundation`CFRunLoopRunSpecific + 572
    frame #7: 0x000000018bf025ec GraphicsServices`GSEventRunModal + 160
    frame #8: 0x0000000184d3b4e0 UIKitCore`-[UIApplication _run] + 992
    frame #9: 0x0000000184d3ffc0 UIKitCore`UIApplicationMain + 112
    frame #10: 0x000000010097f5b0 MyPlayground`main + 192
    frame #11: 0x0000000100b8dc04 dyld_sim`start_sim + 20
    frame #12: 0x0000000100d2d0f4 dyld`start + 520

guard 문

// 1 값 비교
func guardTest(value: Int) {
    guard value = 0 else { return }
        print("안녕하세요.")
    }
}
guardtest(value: 2)

// 2 옵셔널 바인딩
func guardTest(vnale: Int?) {
    guard let value = value else { return }
        print(value)
}

guardTest(value: 2)
guardTest(value: nil)

20. 프로토콜

특정 역활을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진

protocol SomeProtocol {
}

protocol SomeProtocol2 {
}

struct SomeStructure: SomeProtocol, SomeProtocol2 {

}
protocol FirstProtocol {
    var name: Int { get set }
    var age: Int { get }  // 읽기전용
}

protocol AnotoerProtocol {
    static var someTypeProperty: Int { get set }
}

protocol FullyNames {
    var fullName: String { get set }
    func printFullName()
}

struct Person: FullyNames {
    var fullName: String
    func printFullName() {
        print(fullName)
    }
}

protocol SomeProtocol3 {
    func someTypeMethod()
}

protocol SomeProtocol4 {
    init(someParameter: Int)
}

protocol SomeProtocol5 {
    init()
}

class Someclass: SomeProtocol5 {
    required init() {  // 클래스에서는 required 식별자 필요

    }
}

21. Extention

기존의 클래스, 구조체, 열거형, 프로토콜에 새로운 기능을 추가하는 기능

익스텐션이 타입에 추가할 수 있는 기능

  • 연산타입 프로퍼티 / 연산 인스턴트 프로퍼티
  • 타입 메서드 / 인스턴트 메서드
  • 이니셜라이저
  • 서브스크립트
  • 중첩 타입
  • 특정 프로토콜을 준수할 수 있도록 기능 추가
extension Int {

    var isEven: Bool {
        return self % 2 == 0
    }

    var isOdd: Bool {
        return self % 2 == 1
    }
}

var number = 3
number.isEven
number.isOdd


extension String {
    func convertToInt() -> Int? {
        return Int(self)
    }
}

var string = "0"
string.convertToInt()

22. 열거형

연관성 있는 값을 모아 놓은 것을 말한다.

enum CompassPoint: String {
    case north = "북"
    case south = "남"
    case east = "동"
    case west = "서"
}

var direction = CompassPoint.east
direction = .west

switch direction {
case .north:
    print(direction.rawValue)
case .south:
    print(direction.rawValue)
case .east:
    print(direction.rawValue)
case .west:
    print(direction.rawValue)
}

let diretion2 = CompassPoint.init(rawValue: "남")

enum PhoneError {
    case unknown
    case batteryLow(String)
}

let error = PhoneError.batteryLow("베터리가 곧 방전 됩니다.")

switch error {
case .batteryLow(let message):
    print(message)
case .unknown:
    print("알 수 없는 에러입니다. ")
}

23. 옵셔널 체이닝

옵셔널에 속해 있는 nil 일지도 모르는 프로퍼티, 메서드, 서브스크립션 등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정

struct Developer {
    let name: String

}

struct Company {
    let name: String
    var developer: Developer?
}

var developer = Developer(name: "han")
var company = Company(name: "Gunter", developer: developer)
print(company.developer)
print(company.developer?.name)
print(company.developer!.name)

24. try-catch

에러처리란?

프로그램 내에서 에러가 발생한 상황에 대해 대응하고 이를 복구하는 과정

Swift 에러 처리

  • 발생(throwing)
  • 감지(catching)
  • 전파(propagating)
  • 조작(manipulating)
enum PhoneError: Error {
    case unkonwn
    case batteryLow(batteryLevel: Int)
}

throw PhoneError.batteryLow(betteryLevel: 20) // 직접 던지기

/*
Playground execution terminated: An error was thrown and was not caught:
▿ PhoneError
  ▿ batteryLow : 1 element
    - betteryLevel : 20
*/

func checkPhoneBatteryStatus(batteryLevel: Int) throws -> String {
    guard batteryLevel != -1 else { throw PhoneError.unkonwn }
    guard batteryLevel > 20 else { throw PhoneError.batteryLow(batteryLevel: 20) }
    return "베터리 상태가 정상입니다. "
}

// 첫번째 방법
do {
    try checkPhoneBatteryStatus(batteryLevel: 20)
} catch PhoneError.unkonwn {
    print("알수 없는 에러입니다.")
} catch PhoneError.batteryLow(let batteryLevel) {
    print("베터리 전원 부족 남은 베터리: \(batteryLevel)")
} catch {
    print("그 외 오류 발생: \(error)")
}

// 두번째 방법
let status = try? checkPhoneBatteryStatus(batteryLevel: -1)
print(status)

let status2 = try? checkPhoneBatteryStatus(batteryLevel: 30)
print(status2)

25. 클로져

코드에서 전달 및 사용할 수 있는 독립 기능 블록이며, 일급 객체의 역할을 할 수 있음

일급객체 : 전달인자로 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며, 함수의 반환 값이 될 수도 있다.

NameClosure, Unnamed Closure

func hello() {
    print("안녕하세요")
}

let hello2 = { print("안녕하세요") }

클로져 표현식

{ (매개 변수) -> 리턴타입 in
    실행 구문
}
let hello = { () -> () in
    print("hello")
}

hello()

let hello2 = { (name: String) -> String in
    return "Hello, \(name)"
}

//hello2(name: "Gunter") // X: 에러발생
hello2("Gunter")         // O: 전달인자의 값을 적지 않고 보내야함

func doSomething(closer: ()-> ()) {
    closer()
}

doSomething() {
    print("hello2")
}

doSomething {
    print("hello5")
}

doSomething(closer: { () -> () in
    print("hello")
})

func doSomething2() -> () -> () {
    return { () -> () in
        print("hello4")
    }
}

doSomething2()()


func doSomething2(success: () -> (), fail: () -> ()) {

}

doSomething2 {
    <#code#>
} fail: {
    <#code#>
}


// 클로져 간소화
func doSomething3(closure: (Int, Int, Int) -> Int) {
    closure(1, 2, 3)
}

doSomething3(closure: { (a, b, c) in
    return a+b+c
})

doSomething3(closure: {
    return $0+$1+$2 // 약식인수 이름으로 대체
})

doSomething3(closure: {
    $0+$1+$2
})

doSomething3() {
    $0+$1+$2
}

doSomething3 {
    $0+$1+$2
}

26. 고차함수

다른 함수를 전달 인자로 받거나 함수 실행의 결과로 함수로 반환하는 함수

스위프트에서 제공하는 고차함수

  • map
  • filter
  • reduce
// map
let number = [0, 1, 2, 3]
let mapArray = numbers.map { (number) -> Int in
    return number * 2
}
print("map \(mapArray)") // map [0, 2, 4, 6]

// filter
let intArray = [10, 5, 20, 13, 4]
let filterArray = intArray.filter { $0 > 5 }
print("filter \(filterArray)")   //  filter [10, 20, 13]

// 컨네이너 내부 요소를 하나로 통합
let someArray = [1, 2, 3, 4, 5]
let reduceResult = someArray.reduce(0) { // 0 : 초기값
    (result: Int, element: Int) -> Int in
    print("\(result) + \(element)")
    return result + element
}
print("reduce \(reduceResult)") // reduce 15