Reputation: 217
I got two different Objects Song
and Album
. I got an AlbumView
with a @State var album: Album
and I want to reuse that View but pass a Song
instead. Is it possible to pass either Album
or Song
? Otherwise it would also be helpful if I could set Album
to nil and then just check in the View whether it has a value.
This is what Album
looks like:
struct Album: Identifiable {
let id = UUID()
let name: String
let artist: String
let songs: [AlbumSong]
let releaseDate: Date
let price: Int
let albumImageUrl: String
var unlocked: Bool
}
This is what Song
looks like:
struct Song: Identifiable, Codable {
let id = UUID()
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
let price: Int
}
This is my AlbumView
:
struct AlbumView: View {
@State var album: Album
var body: some View {
Text("\(album.name)").font(.system(size: 18))
}
}
This would be my idea to solve it with passing one object as nil:
struct AlbumView: View {
@State var album: Album?
@State var song: Song
var body: some View {
if album != nil {
Text("\(album!.name)")
} else {
Text("\(song.name)")
}
}
}
Upvotes: 1
Views: 90
Reputation: 6032
You can abstract the model (Album
and Song
) away, for example, by using a protocol:
protocol ViewModelProtocol {
var name: String { get }
}
Instead of using Album
or Song
directly, ViewModelProtocol
can be used instead:
struct AlbumView: View {
@State var viewModel: ViewModelProtocol
var body: some View {
Text("\(viewModel.name)").font(.system(size: 18))
}
}
Album
and Song
must conform to the protocol:
// Album and Song both have name property
extension Album: ViewModelProtocol {}
extension Song: ViewModelProtocol {}
Then you just inject whatever you need:
AlbumView(viewModel: Album())
AlbumView(viewModel: Song())
This is just an example of how this can be achieved, other abstractions are possible. With this example you can just add another conformance to the ViewModelProtocol
to enable other model objects to be displayable via the AlbumView
without the need to change AlbumView
.
Example for another model object that does not have a name
property:
struct OtherModel {
var someStringProperty: String
}
extension OtherModel: ViewModelProtocol {
var name: String {
return self.someStringProperty
}
}
Upvotes: 1
Reputation: 2882
Many good answers are already provided. But, if you still want to go ahead with making Album optional, try something like below.
struct AlbumView: View {
@State var album: Album?
@State var song: Song
var body: some View {
if case .some(let myAlbum) = album {
Text("\(myAlbum.name)")
}else{
Text("\(song.name)")
}
}
}
Upvotes: 0
Reputation: 119302
You can make your view dependent to what you are passing by wrapping them in an enum or runtime checking the type of the model but you can abstract out the entire need with a simple protocol:
protocol NameDisplable {
var displayName: String { get }
}
Now you can name anything like:
extension Album: NameDisplable {
var displayName: String { name }
}
extension Song: NameDisplable {
var displayName: String { title }
}
And you view can show anything conformed to this with a little change:
struct AlbumView: View {
let model: NameDisplable
var body: some View {
Text(model.displayName)
}
}
@State
objects are NOT to pass over. They should only hold the internal view's State.? :
instead of if else
inside the view builder. for example:Text("\(albume != nil ? album!.name : song.title)")
nil
from a non-optional property.Upvotes: 1
Reputation: 5084
There are multiple ways to do this:
enum SomeType {
case song(Song)
case album(Album)
}
Then in your View
you pass a type to AlbumView
& display it like so:
struct AlbumView: View {
@State var type: SomeType
var body: some View {
switch type {
case .album(let album):
Text("\(album.name)")
case .song(let song):
Text("\(song.name)")
}
}
}
You pass the type like so:
AlbumView(type: .album(someAlbum))//for albums
AlbumView(type: .song(someSong))//for songs
If you're using shared property names:
protocol SomeMedia {
var name: String {get set}
//any other shared property
}
struct AlbumView: View {
@State var media: SomeMedia
var body: some View {
Text("\(media.name)")
}
}
Make Album
& Song
conform to SomeMedia
, then you can pass a song or an album as you require!
Upvotes: 1
Reputation: 7744
If the view you want to reuse is really that simple use a more generic approach here:
struct TextView: View {
let text: String
var body: some View {
Text(text).font(.system(size: 18))
}
}
and call it with whatever struct you like:
TextView(text: song.title)
or
TextView(text: album.name)
And even if there are more properties you want to use, design the View how it should look like and configure it from the outside. The View does not need to know of the type Album/Song
.
Upvotes: 0