Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #24] 2023 / 05 / 17

Data Base로 Realm을 사용하려 하는데, 이미지를 저장하는 과정에서 문제가 있었습니다.

 

이미지를 데이터 타입으로 변환 후, Realm에서 저장하여 사용해도 되지만

많은 이미지가 추가되면 처리 시간이 오래 걸려서 권장되지 않은 방법이라고 합니다.

또한, 저장할 수 있는 데이터의 크기는 16MB를 초과할 수 없다고 하네요.

 

그래서 일단 FileManager를 통해 이미지를 저장하고

각 이미지의 저장된 경로와 메타데이터를 통해 불러오는 방식을 구현해보려 합니다.

(Realm에서는 이미지를 식별할 수 있는 이름만 저장하면 될 것 같습니다.)

 

 

FileManager로 이미지 저장

공식문서와 다른 블로그에 설명이 잘 돼있기 때문에 FileManager에 대한 설명은 생략하겠습니다.

코드에서 디렉터리(Directory)가 많이 보이는데, 이건 파일 이름을 포한한 경로라고 생각하시면 됩니다. (파일 URL 경로)

 

간단히 사용방법을 설명하면

이미지의 파일 경로로 접근한 다음, 동작들을 처리해 주면 됩니다.

 

 

2023 / 09 / 13 새로운 문제 발견

무슨 이유인지 모르겠는데, appendingPathComponent(:conformingTo:) 메서드가 사용이 안되네요.

 

👉 해결 방법

그냥 직접 사용할 확장자명을 입력해 주면 됩니다. (jpeg, txt 등)

appendingPathComponent("imageName.jpeg")

// 또는 iOS 16+ 부터 새로 도입된 제너릭으로 만들어진 메서드 사용
appending(path: "imageName.jpeg")

 

 

이미지 저장 방법

설명은 주석을 참고해 주세요.

func saveImageToDirectory(identifier: String, image: UIImage) {
    // 저장할 디렉토리 경로 설정 (picturesDirectory, cachesDirectory도 존재하지만 Realm과 같은 경로에 저장하기 위해서 documentDirectory 사용함.)
    // userDomainMask: 사용자 홈 디렉토리는 사용자 관련 파일이 저장되는 곳입니다.
    let documentsDirectory = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first!
    // Realm에서 이미지에 사용될 이름인 identifier를 저장 후, 사용하면 됩니다
    let imageName = "\(identifier)"
    // 이미지의 경로 및 확장자 형식 (conformingTo: 확장자)
    let fileURL = documentsDirectory.appendingPathComponent(imageName, conformingTo: .jpeg)
    
    // Directory 경로라고 했죠? 파일이 저장된 위치를 확인하고 싶을 때, 단순히 경로를 프린트해서 확인이 가능합니다. 
    print(fileURL)
    
    do {
        // 파일로 저장하기 위해선 data 타입으로 변환이 필요합니다. (JPEG은 압축을 해주므로 크기가 줄어듭니다. PNG는 비손실)
        if let imageData = image.jpegData(compressionQuality: 1) {
            // 이미지 데이터를 fileURL의 경로에 저장시킵니다.
            try imageData.write(to: fileURL)
            print("Image saved at: \(fileURL)")
        }
        
    } catch {
        print("Failed to save images: \(error)")
    }
}

 

 

이미지 읽기

이미지 저장 방식과 비슷합니다.

func loadImageFromDirectory(with idnetifier: String) -> UIImage? {
    let fileManager = FileManager.default
    // 파일 경로로 접근
    let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
    let fileURL = documentsDirectory.appendingPathComponent(idnetifier, conformingTo: .jpeg)
    
    // 이미지 파일이 존재한다면, 이미지로 변환 후 리턴
    guard fileManager.fileExists(atPath: fileURL.path) else { return nil }
    
    return UIImage(contentsOfFile: fileURL.path)
}

 

한 개의 파일을 아닌 디렉터리 내부의 이미지 파일들 전부를 읽고 싶다면

contentsOfDirectory 메서드를 이용하면 됩니다.

(편하게 필터링하기 위해 Path 대신 URL로 접근해 줬습니다.)

func loadAllImageFromDirectory() -> [UIImage]? {
    let fileManager = FileManager.default
    // 파일 경로로 접근
    let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

    do {
        // 디렉토리 내부의 콘텐츠들에 접근
        let fileURLs = try fileManager.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
        // 이미지 path만 필터링
        let imageURLs = fileURLs.filter { $0.pathExtension.lowercased() == "jpeg" }
        // 이미지들로 변환
        let images = imageURLs.compactMap { UIImage(contentsOfFile: $0.path) }

        return images
    
    } catch {
        print("Error reading directory \(error)")
    }
    
    return nil
}

 

 

이미지 삭제

func deleteImageFromDirectory(idnetifier: String) {
    let fileManager = FileManager.default
    let documuentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
    let fileURL = documuentDirectory.appendingPathComponent(idnetifier, conformingTo: .jpeg)
    
    do {
        try fileManager.removeItem(at: fileURL)
        print("Successfully deleted image")
    } catch {
        print("Failed to delete image: \(error)")
    }
}

 

 

이미지 수정

replaceItem란 메서드도 존재하는데... 이건 문제가 있네요.

말 그대로 대체를 하는 거라 그런지, 이미지의 이름은 그대로 유지되고 이미지만 변경되네요.

func replaceImageFromDirectory(at oldIdentifier: String, with newIdentifier: String) {
    let fileManager = FileManager.default
    let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
    let oldFileURL = documentsDirectory.appendingPathComponent(oldIdentifier, conformingTo: .jpeg)
    let newFileURL = documentsDirectory.appendingPathComponent(newIdentifier, conformingTo: .jpeg)

    do {
        try fileManager.replaceItem(at: oldFileURL, withItemAt: newFileURL, backupItemName: nil, resultingItemURL: nil)
        print("Successfully replace image")

    } catch {
        print("Failed to replace image: \(error)")
    }
}

 

수정하는 메서드는 따로 없으니,

위에서 사용했던, 생성과 제거를 이용해서 수정하는 메서드를 직접 만들어주면 될 것 같습니다.

func replaceImageFromDirectoryV2(at oldIdentifier: String, with newIdentifier: String) {
    deleteImageFromDirectory(idnetifier: oldIdentifier)
    saveImageToDirectory(identifier: newIdentifier, image: image)
}

 

 

새로운 폴더 생성 방법

사용 방법은 위의 파일 추가하는 방법이랑 거의 비슷합니다.

createDirectory - 경로에 폴더를 생성합니다.

func saveImageToDirectory(identifier: String, image: UIImage) {
    
    let fileManager = FileManager.default
    // Documents 폴더의 디렉토리
    let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
    // Documents 폴더에서 images 디렉토리 추가 (현재 경로는 이렇게 됩니다. ./Documents/images)
    let imagesFolderDirectory = documentsDirectory.appendingPathComponent("images")
    
    // Documents 폴더는 무조건 있으니까 넘어가고 만약, 이미지 디렉토리가 없다면 이미지 폴더를 생성
    if !fileManager.fileExists(atPath: imagesFolderDirectory.path) {
        do {
            try fileManager.createDirectory(at: imagesFolderDirectory, withIntermediateDirectories: true)
        } catch {
            print("Failed to create folder")
        }
    }
    
    // 폴더를 생성해 줬으니, 위에서 사용한 이미지를 저장하는 코드를 추가해주면 됩니다.
    ...
}

 

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!