궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFTUI/Image

SwiftUI : AsyncImage [Placeholder,Extension,Phase,Transaction]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

AsyncImage 에 대해 알아보도록 합시다.

 

AsyncImage

iOS 15로 업데이트되면서 추가된 기능인데, AsyncImageURLSession 인스턴스를 사용하여 할당된 URL에서 이미지를 가져오는 기능을 가지고 있다.

 

디바이스를 실행하고 몇 초의 시간 후 이미지가 나타나게 하려면 AsyncImage를 사용할 수 있다.

 

먼저 간단하게 Url Image를 코드작성한다.

import SwiftUI

struct ContentView: View {
    private let imageName: String = "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlI00H%2FbtqXQ6wtwVP%2FUrgHPkbHKxeqWwHIYzoaK1%2Fimg.png"
    
    var body: some View {
        AsyncImage(url:URL(string: imageName)) 
    }
}

앱을 실행시켜보면 몇 초의 지연 후 이미지가 나타나지만, 확대되어 나오는 것을 확인할 수 있다.

Scale 

    var body: some View {
        AsyncImage(url:URL(string: imageName), scale: 2.0)

Scale의 값이 클수록 이미지의 크기가 작아진다.

PlaceHolder

placeHolder를 사용하여 어떠한 아이콘 또는 이미지가 먼저 화면에 나온 뒤, AsyncImage가 출력될 수 있도록해 줄 수 있다.

    var body: some View {
        AsyncImage(url:URL(string: imageName)) { image in
            image
                .resizable()
                .scaledToFit()
        } placeholder: {
            Image(systemName: "paperplane.circle.fill")
                .resizable()
                .scaledToFit()
                .frame(maxWidth: 200)
                .foregroundColor(.blue.opacity(0.6))
        }
        .padding(20)
    }

Resizable과 같은 수정자는 AsyncImage에 직접 사용이 불가하므로 Image 인스턴스에 적용해야 한다.

Image Extenstion

작성한 코드를 살펴보면 이미지의 수정자 중 중복되는 코드가 두 가지나 있다. 코드를 작성할 때 중복 코드는 상당히 보기 좋지 않다. 이럴 때 사용 가능한 Extenstion 프로퍼티이다. 

 

비동기 이미지와 Icon에 있는 .resizable().scaledToFit() 수정자를 다음과 같이 코드를 생성할 수 있다.

extension Image {
    func ImageModifier() -> some View {
        self
            .resizable()
            .scaledToFit()
    }
}

ImageModifier 메서드의 반환 타입some View로 지정해주고 그 안에 수정자를 포함시켰다.

 

모든 인스턴스는 암시적으로 생성된 self 프로퍼티를 갖는데, 바로 인스턴스 자기 자신을 가리키는 프로퍼티이다.

 

Image수정자 메서드는 작성했고, 이제 IconModifier도 추가해준다.

extension Image {
    func ImageModifier() -> some View {
        self
            .resizable()
            .scaledToFit()
    }
    
    func IconModifier() -> some View {
        self
            .ImageModifier()
            .frame(maxWidth: 200)
            .foregroundColor(.blue.opacity(0.6))
    }
}

새로운 IconModifier 메서드에 ImageModifier를 포함시킬 수 도 있다. 

호출 방법

Image extension을 호출은 아래와 같이 할 수 있고, 코드는 간결해지고 보기도 쉬워졌다.

    var body: some View {
        AsyncImage(url:URL(string: imageName)) { image in
            image.ImageModifier()
            
        } placeholder: {
            Image(systemName: "paperplane.circle.fill")
                .IconModifier()
        }
        .padding(20)
    }

AsyncImagePhase (Phase)

init(url: scale: transaction: content: ) 이니셜라이저를 사용하여 AsyncImage 인스턴스를 생성할 때, 현재 상태를 로드하는 동안 이 클로저를 호출하게 된다. 이 단계에서 몇 가지의 상태를 파악할 수 있게 되는 것이다.

기본 코드

if let

AsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
    if let image = phase.image {
        .resizable()
        .aspectRatio(contentMode: .fit)
        .clipShape(RoundedRectangle(cornerRadius: 15))
        .padding() // Displays the loaded image.
    } else if phase.error != nil {
        Text(.localizedDescription)
    } else {
        emptyView() // Acts as a placeholder.
    }
}

switch

AsyncImage(url: URL(string: imageName)) { phase in
    switch phase {
    case .success(let image):
        image
         .resizable()
         .padding()
        
    case .failure(let error):
        VStack {
            Image(systemName: "photo.circle.fill")
                .IconModifier()
            Text("error code : \(error.localizedDescription)")
                .padding()
                .lineLimit(3)
        }
        
    case .empty:
        Image(systemName: "photo.circle.fill")
            .IconModifier()
        
    @unknown default:
        defaultView()
    }
}

사용 방법

AsyncImage는 이렇게 사용 가능하다.

 

if let

import SwiftUI

extension Image {
    func ImageModifier() -> some View {
        self
            .resizable()
            .scaledToFit()
    }
    
    func IconModifier() -> some View {
        self
            .ImageModifier()
            .frame(maxWidth: 200)
            .opacity(0.6)
    }
}

struct ContentView: View {
    private let imageName: String = "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlI00H%2FbtqXQ6wtwVP%2FUrgHPkbHKxeqWwHIYzoaK1%2Fimg.png"
    var body: some View {
        AsyncImage(url: URL(string: imageName)) { phash in
             //SUCCESS : 이미지 로드 성공
             //FAILURE : 이미지 로드 실패 에러
             //EMPTY : 이미지 없음. 이미지가 로드되지 않음
            if let image = phash.image {
                image.ImageModifier()
            } else if phash.error != nil {
                Image(systemName: "exclamationmark.icloud.fill").IconModifier().foregroundColor(.red)
            } else {
                Image(systemName: "photo.circle.fill").IconModifier().foregroundColor(.blue)
            }
        }
    }
}

switch

var body: some View {
    AsyncImage(url: URL(string: imageName)) { phase in
        switch phase {
          //SUCCESS : 이미지 로드 성공
          //FAILURE : 이미지 로드 실패 에러
          //EMPTY : 이미지 없음. 이미지가 로드되지 않음
        case .success(let image):
            image.ImageModifier()
        case .failure(_):
            Image(systemName: "exclamationmark.icloud.fill").IconModifier().foregroundColor(.red)
        case .empty:
            Image(systemName: "photo.circle.fill").IconModifier().foregroundColor(.blue)
        
        @unknown default:
            //Image(systemName: "photo.circle.fill").IconModifier()
            ProgressView()
        }
    }
}

1. 이미지 로드 성공  /  2. Error  /  3. placeholder 및 default 값

Animation / Transaction

init(url: scale: transaction: content: )

AsyncImageAnimation 효과를 주려면 다음과 같이 작성할 수 있다. transition 효과에 대해 자세히 보려면 여기를 클릭하세요.

TIP
 
 

transaction 옵션
response - dampingFraction이 0일 때 하나의 진동을 완료하는 데 걸리는 시간
dampingFraction - spring이 얼마나 빨리 멈추는지 제어
➜ 값이 0이면 animation이 멈추지 않음
blendDuration - animation 사이 transition의 길이를 정함
➜ 값이 0일 경우 blending을 끔

 

var body: some View {
    AsyncImage(url: URL(string: imageName), transaction: Transaction(animation: .spring(response: 0.7, dampingFraction: 0.5, blendDuration: 0.3))) { phase in
        switch phase {
        case .success(let image):
            image.imageModifier()
                .transition(.move(edge: .bottom)) //아래에서 위로 튀어오르는 효과
        case .failure(_):
            Image(systemName: "exclamationmark.icloud.fill")
                .iconModifier()
        case .empty:
            Image(systemName: "photo.circle.fill")
                .iconModifier()
        @unknown default:
            ProgressView()
        }
    }
    .padding(30)
}

종류는 아래 말고도 다양하게 많이 있으니 써보는 것을 추천!

image.imageModifier()
    .transition(.move(edge: .bottom))
    .transition(.slide)
    .transition(.scale)
    .transition(AnyTransition.scale.animation(.easeInOut))

결과를 확인할 때, 캔버스의 프리뷰에서는 애니메이션 효과가 나타나지 않기 때문에 + R 단축키를 사용해 시뮬레이터에서 빌드해야 한다.

 

TIP
 
 

AsyncImage
AsyncImage는 iOS 15 이상부터 사용 가능합니다.

 

 

읽어주셔서 감사합니다🤟

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.