1. Publisher

Publisher 는 값을 발행하는 발행자를 말함. 구독자는 Publisher 가 방출하는 값을 전달받을 수 있음.

import Foundation
import Combine

//publisher
let publisher = [0,1,2,3,4,5].publisher

//subscriber가 구독자가 방출하는 값을 받아 그 중 짝수만을 프린트함
let subscriber = publisher
    .filter {
        $0 % 2 == 0
    }
    .sink { intValue in
        print("\(intValue)")
    }

2. Cancellable

구독을 취소할 수 있는 타입. 컴바인에서 퍼블리셔를 구독하게 되면 Cancellable(혹은 AnyCancellable) 을 반환하며 .cancel() 메서드를 이용하여 언제든 구독을 취소할 수 있음.

import Foundation
import Combine

//매 1초마다 현재 시간을 방출하는 컴바인 타이머
let timer = Timer.publish(every: 1, on: .current, in: .default).autoconnect()

//구독자가 매 초마다 타이머의 값을 프린트함
let timerSubscriber: AnyCancellable = timer.sink { time in
    print(time)
}

//5초 후 구독을 종료함
Task {
    DispatchQueue.global(qos:.default).asyncAfter(deadline: .now() + 5) {
        timerSubscriber.cancel()
        print("Subscription is cancelled.")
    }
}

3. PassthroughSubject

PassthroughSubject의 특징은 아래와 같음.

1. 초기값을 가지지 않음
2. 구독자는 구독한 이후 변경되는 값부터 받아볼 수 있음

import Foundation
import Combine

//PassthroughSubject Publisher로 첫번째 제네릭은 전달할 타입, 두번째는 에러 타입.
let publisher = PassthroughSubject<Int, Never>()

//첫번째 구독자
let subscriber1: AnyCancellable = publisher.sink { integer in
    print("integer1 is: \(integer)")
}

publisher.send(123) //구독자1이 123을 출력

//두번째 구독자
let subscriber2 = publisher.sink { integer in
    print("integer2 is: \(integer)")
}

publisher.send(456) //두 구독자 모두 456을 출력
publisher.send(789) //두 구독자 모두 789를 출력

/*
 위의 순서에 따른 프린트 결과:
 integer1 is: 123
 integer1 is: 456
 integer2 is: 456
 integer1 is: 789
 integer2 is: 789
*/

4. CurrentValueSubject

CurrentValueSubject의 특징은 아래와 같음.

1. 초기값을 가짐
2. 구독자는 구독과 동시에 즉시 값을 전달받을 수 있음
3. 새로운 값을 방출할 때마다 구독자에게 자동으로 알림

import Foundation
import Combine

//PassthroughSubject Publisher로 첫번째 제네릭은 전달할 타입, 두번째는 에러 타입.
let publisher = CurrentValueSubject<Int, Never>(100) //100은 initial value

//첫번째 구독자
let subscriber1: AnyCancellable = publisher.sink { integer in
    print("integer1 is: \(integer)") //초기값 100은 이곳에서 한번만 프린트됨
}

publisher.send(200) //갱신된 값 200을 구독자1이 프린트함

//두번째 구독자
let subscriber2 = publisher.sink { integer in
    print("integer2 is: \(integer)") //구독자 2가 새로 설정된 초기값 200을 프린트함
}

publisher.send(300) //구독자 1,2가 모두 갱신된 값 300을 프린트함

/*
 위의 순서에 따른 프린트 결과:
 integer1 is: 100
 integer1 is: 200
 integer2 is: 200
 integer1 is: 300
 integer2 is: 300
*/

이렇게 써놔야 까먹어도 또 볼수있겠지...


WRITTEN BY
artfrige
베이스 연주는 건강에 좋습니다
,

할말은 주석에 다 써놨다.

// import Combine 까먹지 말것
import UIKit
import Combine

// UIKit에서는 ObservableObject 프로토콜을 채택 안해도 동작함.
// SwiftUI에서는 @StateObject 로 ViewModel 을 선언해야 하기 때문에 꼭 필요함.
// 예제에서는 그냥 멋있어 보이려고 채택함
class TimerClass: ObservableObject {
    
    // 프로퍼티에 @Published 프로퍼티 래퍼를 달아주면 값을 발행(publish) 할 수 있게 됨
    @Published var count: Int = 0
    var timer: Timer = Timer()
    
    init() {
        initTimer()
    }
    
    func initTimer() {
        //1초에 한번 publish 하는 값을 +1씩 갱신
        self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] _ in
            guard let self = self else { return }
            self.count += 1
        })
    }
    
    func stopTimer() {
        self.timer.invalidate()
    }
}

class CombineTimerExampleViewController: UIViewController {
    
    let timerViewModel: TimerClass = TimerClass()
    let timerCount: UILabel = UILabel()
    let stopButton: UIButton = UIButton()
    
    //구독자의 타입인 Cancellable이 담길 곳
    var cancellable: AnyCancellable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //뷰 셋업
        setupView()
        
        //뷰 바인딩
        bind()
    }
    
    private func bind() -> () {
        // 뷰에 갱신될 값은 구독 함수인 sink() 를 통해서 구독됨.
        // sink()가 방출하는 값은 AnyCancellable 타입이고, 이걸 위해 만들어둔 self.cancellable에 담고
        // sink()클로저에 어느 뷰와 묶어줄지만 선언해주면 알아서 값이 바뀔 때마다 뷰가 갱신됨.
        // 주의해야 할 점은 뷰 갱신을 위해 @Published 프로퍼티를 받아올 때에는 $수정자를 붙여야 함 (.count 가 아니고 .$count)
        self.cancellable = timerViewModel.$count.sink { [weak self] count in
            guard let self = self else { return }
            self.timerCount.text = "\(count)"
        }
    }
    
    private func setupView() -> () {
        timerCount.textAlignment = .center
        timerCount.font = UIFont.systemFont(ofSize: 30)
        timerCount.translatesAutoresizingMaskIntoConstraints = false
        
        stopButton.configuration = .borderedProminent()
        stopButton.configuration?.title = "Stop Timer"
        stopButton.addTarget(self, action: #selector(stopTimer), for: .touchUpInside)
        stopButton.translatesAutoresizingMaskIntoConstraints = false
        
        self.view.backgroundColor = UIColor.systemBackground
        self.view.addSubview(timerCount)
        self.view.addSubview(stopButton)
        
        NSLayoutConstraint.activate([
            timerCount.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            timerCount.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
            timerCount.widthAnchor.constraint(equalTo: self.view.widthAnchor, constant: -40),
            timerCount.heightAnchor.constraint(equalToConstant: 40),
            
            stopButton.topAnchor.constraint(equalTo: timerCount.bottomAnchor, constant: 20),
            stopButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
        ])
    }
    
    @objc func stopTimer() {
        //타이머 멈추기
        timerViewModel.stopTimer()
    }
    
}

WRITTEN BY
artfrige
베이스 연주는 건강에 좋습니다
,

엑스코드에서 프로젝트 이름을 바꾸며 내부 디렉토리의 디렉토리 이름까지 전부 바꾸는 바람에 dependency 경로에 문제가 생겨서 프로젝트를 수동으로 옮길 뻔 했는데..
다행히 구글의 한 용자 덕분에 나의 시간과 노력을 아낌.

~/Library/Developer/Xcode/DerivedData/
  1. 문제가 있는 Dependency를 삭제한 후 엑스코드를 종료함
  2. 파인더에서 위의 경로로 이동한 다음 DerivedData 디렉토리 안의 문제가 있는 Dependency에 관련된 디렉토리를 지움
    (혹시 모르면 다 지우던가..)
  3. XCode 를 실행하고 다시 Dependency를 설치.
  4. PROFIT!

이래도 안되면? 나도몰루...


WRITTEN BY
artfrige
베이스 연주는 건강에 좋습니다
,

ㅈ바스크립트에서 await 은 Promise 를 방출하는 놈에 달아줄 수 있다

async function printData(){
    let endpoint = "https://jsonplaceholder.typicode.com/posts/1"
    let dataset = await fetch(endpoint).then((res) => { return res.json() })

    try {
        console.log(dataset)
    } catch(e) {
        console.log(e)
    }
}

printData() //쨘 json 데이터 프린트 성공

위 코드에서 fetch 가 Promise 를 방출하기 때문에 await 키워드를 달아서 파싱된 json을 dataset 에 담아 줌.
그렇기 때문에 try 문에서 받아온 json 데이터를 .then()으로 Promise 해결하지 않고도 사용 가능.

async/await 을 쓰는 이유야 여러개 있겠지만, 그 중 최고의 이유는 .then() 체인조차도 마지막엔 콜백지랄을 해야되기 때문에 어떻게든 불편할 수밖에 없었던 우리의 마음을 달래기 위한 최고의 솔루션이기 때문이지 않나 싶고...


WRITTEN BY
artfrige
베이스 연주는 건강에 좋습니다
,
$composer require vlucas/phpdotenv

을 프로젝트 루트 디렉토리에서 실행하면 의존성에 phpdotenv가 주입됨
그다음 index.php에서 아래 코드를 최상단에 추가함

require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

그다음 프로젝트 root디렉토리에 .env 파일을 생성하고 아래와 같이 불러 쓰면됨

$someEnvVariable = $_ENV["YOUR_ENV_VARIABLENAME"]

composer 가 설치되지 않았거나 설치하지 않은 채로 적용하고 싶다면 아래 github repository를 참조할 것.
https://github.com/agungjk/phpdotenv-for-codeigniter


WRITTEN BY
artfrige
베이스 연주는 건강에 좋습니다
,