enter image description here

enter image description here

Lo que me gustaría hacer es darle al usuario la opción de elegir si la widget background es una imagen tomada de http o una gradient background.

Actualmente tengo la siguiente estructura de notas, pero no puedo hacer que funcione.

Entonces typeBg debe tener un valor predeterminado, si no se pasa, debería tomar el valor predeterminado.

Los valores de imagen y bgColors deben ser parámetros opcionales.

struct Note: Identifiable, Codable {
    let title: String
    let message: String
    let image: String?
    let bgColors: [Color?]//[String?]
    let typeBg: String? = "color"
    
    var id = UUID()
}

Pero solo obtengo errores, en la estructura Nota:

El tipo 'Nota' no se ajusta al protocolo 'Decodificable'

El tipo 'Nota' no se ajusta al protocolo 'Codificable'

Lo que me gustaría hacer es:

Si typeBg del Struct == 'url', entonces tomo como valor image que es una URL.

Si typeBg del Struct == 'gradient', entonces tomo como valor bgColors que es una matriz de Color.

ContentView:

SmallWidget(entry: Note(title: "Title", message: "Mex", bgColors: bgColors, typeBg: "gradient"))

SmallWidget:

struct SmallWidget: View {
    var entry: Note
    @Environment(\.colorScheme) var colorScheme
    
    
    func bg() -> AnyView { //<- No work
        switch entry.typeBg {
        case "url":
            return AnyView(NetworkImage(url: URL(string: entry.image))
        case "gradient":
            return AnyView(
                LinearGradient(
                    gradient: Gradient(colors: entry.bgColors),
                    startPoint: .top,
                    endPoint: .bottom)
            )
        default:
            return AnyView(Color.blue)
        }
        
        var body: some View {
            GeometryReader { geo in
                VStack(alignment: .center){
                    Text(entry.title)
                        .font(.title)
                        .bold()
                        .minimumScaleFactor(0.5)
                        .foregroundColor(.white)
                        .shadow(
                            color: Color.black,
                            radius: 1.0,
                            x: CGFloat(4),
                            y: CGFloat(4))
                    Text(entry.message)
                        .foregroundColor(Color.gray)
                        .shadow(
                            color: Color.black,
                            radius: 1.0,
                            x: CGFloat(4),
                            y: CGFloat(4))
                    
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)
            }
            .background(bg)
            //.background(gradient)
            //.background(NetworkImage(url: URL(string: entry.image)))
        }
    }
struct NetworkImage: View {
    
    public let url: URL?
    
    var body: some View {
        Group {
            if let url = url, let imageData = try? Data(contentsOf: url),
               let uiImage = UIImage(data: imageData) {
                
                Image(uiImage: uiImage)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            }
            else {
                ProgressView()
            }
        }
        
    }
}
1
Paul 24 oct. 2020 a las 20:00

1 respuesta

La mejor respuesta

Esto tomó bastante tiempo, porque Color no es Codable, por lo que se tuvo que hacer una versión personalizada. Esto es lo que obtuve:

struct Note: Identifiable, Codable {
    
    enum CodingKeys: CodingKey {
        case title, message, background
    }
    
    let id = UUID()
    let title: String
    let message: String
    let background: NoteBackground
}


extension Note {
    
    enum NoteBackground: Codable {
        
        enum NoteBackgroundError: Error {
            case failedToDecode
        }
        
        case url(String)
        case gradient([Color])
        
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            
            if let url = try? container.decode(String.self) {
                self = .url(url)
                return
            }
            if let gradient = try? container.decode([ColorWrapper].self) {
                self = .gradient(gradient.map(\.color))
                return
            }
            
            throw NoteBackgroundError.failedToDecode
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            
            switch self {
            case let .url(url):
                try container.encode(url)
            case let .gradient(gradient):
                let colors = gradient.map(ColorWrapper.init(color:))
                try container.encode(colors)
            }
        }
    }
}

Para hacer que Color sea Codable, está envuelto en ColorWrapper:

enum ColorConvert {
    
    struct Components: Codable {
        let red: Double
        let green: Double
        let blue: Double
        let opacity: Double
    }
    
    static func toColor(from components: Components) -> Color {
        Color(
            red: components.red,
            green: components.green,
            blue: components.blue,
            opacity: components.opacity
        )
    }
    
    static func toComponents(from color: Color) -> Components? {
        guard let components = color.cgColor?.components else { return nil }
        guard components.count == 4 else { return nil }
        let converted = components.map(Double.init)
        
        return Components(
            red: converted[0],
            green: converted[1],
            blue: converted[2],
            opacity: converted[3]
        )
    }
}


struct ColorWrapper: Codable {
    
    let color: Color
    
    init(color: Color) {
        self.color = color
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let components = try container.decode(ColorConvert.Components.self)
        color = ColorConvert.toColor(from: components)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let components = ColorConvert.toComponents(from: color)
        try container.encode(components)
    }
}

Luego se puede usar así:

struct ContentView: View {
    
    let data = Note(title: "Title", message: "Message", background: .url("https://google.com"))
    //let data = Note(title: "Title", message: "Message", background: .gradient([Color(red: 1, green: 0.5, blue: 0.2), Color(red: 0.3, green: 0.7, blue: 0.8)]))
    
    var body: some View {
        Text(String(describing: data))
            .onAppear(perform: test)
    }
    
    private func test() {
        do {
            let encodedData = try JSONEncoder().encode(data)
            print("encoded", encodedData.base64EncodedString())
        
            let decodedData = try JSONDecoder().decode(Note.self, from: encodedData)
            print("decoded", String(describing: decodedData))
        } catch let error {
            fatalError("Error: \(error.localizedDescription)")
        }
    }
}

Nota: el Color que codifica no puede ser algo como Color.red; tiene que estar hecho a partir de los componentes RGB, como usar el inicializador Color(red:green:blue:).

En su caso, podría hacer algo como esto para cambiar el fondo en función de entry background:

@ViewBuilder func bg() -> some View {
    switch entry.background {
    case let .url(url):
        NetworkImage(url: URL(string: url))
    case let .gradient(colors):
        LinearGradient(
            gradient: Gradient(colors: colors),
            startPoint: .top,
            endPoint: .bottom
        )
        
    /// CAN ADD ANOTHER CASE TO `NoteBackground` ENUM FOR SOLID COLOR HERE
    }
}
2
George_E 25 oct. 2020 a las 15:27