1

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)")
        }
    }
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
AdamLeet
  • 127
  • 8

5 Answers5

1

There are multiple ways to do this:

Enum

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

Protocol

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!

Timmy
  • 4,098
  • 2
  • 14
  • 34
  • How would I pass something to AlbumView? If I try to pass an Album it tells me I cannot convert Album to SomeType – AdamLeet Sep 05 '22 at 12:46
  • 1
    you pass it like: `.album(yourAlbum)` – Timmy Sep 05 '22 at 12:47
  • Note, that with the enum variant, `AlbumView` is now dependant on both `Song` and `Album`. If one wanted to add another model to be used in `AlbumView` - `AlbumView` needs to be changed, if `Song` or `Album` changes - `AlbumView` might need to be changed. And when a change is required it might get to be not easy to find exactly what `AlbumView` expects from `Song` or `Album`. – MANIAK_dobrii Sep 05 '22 at 13:20
1

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)
    }
}

Some Notes About Your Code

  1. @State objects are NOT to pass over. They should only hold the internal view's State.
  2. Try NOT to branch the view (more information here). Use ? : instead of if else inside the view builder. for example:
Text("\(albume != nil ? album!.name : song.title)")
  1. Try moving the conditions from runtime checks into the compile time check.
  2. Try NOT to couple your code. Use protocols and abstraction instead.
  3. You can NOT expect nil from a non-optional property.
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • Thanks for the Notes. Understanding the differences between StateObject, EnvironmentObject, Binding, ObservableObject and State is hard since I am quite new to swift. I do understand that State is for the internal view but since I don't need my parameter to change neither for it to be observed I just used State anyways. Would EnvironmentObject be the right way? – AdamLeet Sep 05 '22 at 13:03
  • 1
    You should pass read-only objects as a normal variable. – Mojtaba Hosseini Sep 05 '22 at 13:12
1

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
    }
}
MANIAK_dobrii
  • 6,014
  • 3
  • 28
  • 55
  • Does this also work if Album and Song got different parameters? – AdamLeet Sep 05 '22 at 12:52
  • @AdamLeet Yes, it does, as long as the model contains something (e.g. string) that can be used as name. The conformance to the ViewModelProtocol would just be a bit more complex, like: `var name: { return self.otherStringVariable }`. If concrete model objects are too different I would suggest to extract a dedicated view model struct/class (e.g. `struct AlbumViewModel`) and a way to create this object from concrete models. – MANIAK_dobrii Sep 05 '22 at 13:05
0

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.

burnsi
  • 6,194
  • 13
  • 17
  • 27
0

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)")
            }
        }
    }
Tushar Sharma
  • 2,839
  • 1
  • 16
  • 38