Hey, thanks for your detailed answer. Now that you explained why, it makes sense to have Swift 3 and Swift 4 version separate. Still, I will try to keep up with the changes that you're doing in Swift 3 and try to bring them up to Swift 4. I'm an update junkie and always have to have the latest and greatest. For me, having to figure things out on my own is also part of the deal, and it's a thing I really like.
Regarding the window controls I'm just experimenting, they are gone as easy as two clicks, so I didn't really care. But a native close button for the playlist makes total sense in my opinion. How would it make things more complicated, when using native controls? Also it's often quicker in terms of mouse movement when having a dedicated close button at the playlist window itself.
Another thing i maybe want to change in terms of UX is having the playlist resize and docking controls on the main window. Besides that there's more things I just wanna try and play with (like media keys, menu bar icon...), knowing I can always go back to how it was.
Yeah, I'm also very interested in what you're doing on your side. It is already my favorite music player on mac xD and I found out about it not even two days ago. Haven't even really dug deep into the code and already learned so much from your work.
Oh, and just want to point out that somehow your custom minimize button does not work for me. Nothing happens when I press it.
You're a very fast learner !
Yes, I'm very much open to doing things a different way. Feel free to experiment with moving the window controls around as you wish ... as long as they're intuitive to the end user.
What I was trying to say about the playlist window controls was that the 3 custom resize buttons behave differently than the native green maximize control. So, this represents a disparity or inconsistency. Consistency is good from a UX perspective. When the user clicks a button, he/she should know exactly what it is going to do. If the custom maximize button expands the window only vertically and the native control expands it both vertically and horizontally, that is inconsistent behavior ... not a good user experience.
Take a look in WindowViewController (or better yet, just run the app and use the playlist maximize controls) ... the custom maximize button does not behave the same way as the native maximize button. The custom button will expand the playlist window either to the left/right of the main app window, or above or below it (depending on the relative locations of both windows), so that both are always visible and do not overlap. This is the intended maximize behavior. On the other hand, the native control does not know or care about the main app window and will maximize it over the entire screen. This is not really the intended behavior.
Also, I don't know if the OS controls will do something weird with the window, i.e. unexpected behavior, in some cases that we have not foreseen. By allowing control only through custom controls, you take the uncertainty out of the equation, and everything behaves in very predictable and consistent ways as you have programmed it. That's why I didn't like the playlist window having native controls.
But again, experiment away, and if you find that the new layout of controls is intuitive and behaves predictably, then, that's fine too. I'm open to new ways of doing things ... absolutely.
At the moment, I'm working on playlist drag/drop re-ordering. It is quite involved, as it turns out ! Another thing I'm looking to add is new playlist views that will group the tracks by album/artist/genre, etc, like iTunes does. I may or may not do it. The playlist could have a tab group, one for each grouped view, plus the default flat view.
BTW, my GitHub page has a "Planned Updates" section on the main page. If you're ever wondering what I'm up to, that's a great place to check
🙂 Just FYI.
The custom minimize button calls miniaturize() which is supposed to hide/minimize the window. Weird. Maybe Swift 4 has a new way of doing that.
[doublepost=1508220218][/doublepost]
------------------------------------------------------------------------------------------
Since my developer-readme file is totally outdated at this point, and since you're actively working on the code, I just wanted to give you a few pointers into my code (although I'm aware that you like to figure things out on your own). I will also update developer-readme soon.
App layers: The app definitely implements the MVC design pattern, but I like to call them "View", "Delegate", and "Back end"
The view layer consists of several ViewController classes, one for each logical component of the app (not necessarily physical component, but logical component). For instance, all playback-related functions are handled by PlaybackViewController. And, there are several custom views, e.g. XXXSliderCell, that customize the look and feel of UI controls.
PlaylistTableViewController: This class is so important it deserves a special mention. This class provides the data for the playlist table view. It also handles drag/drop operations.
The delegate layer takes requests from the view layer, and marshals them into formats the back end can use. It does things like conversion of values from a UI format to the audio engine's format, and formatting of back end values into UI-friendly formats. E.g. volume in the UI ranges from 0-100, but the engine requires 0-1. Generally, the delegate layer provides the view layer with a simple interface for accessing the audio engine in the back end, and provides app-level operations that then get translated into audio-engine-level operations. The delegate layer consists of several protocols, that are named XXXDelegateProtocol, which provide the contracts, and of course their concrete implementations. One rule that is always followed is that the view layer never accesses the back end directly; it must always only interact with the delegate layer.
Finally,
the back end, also accessed through protocols, consists of 3 main components:
1 - Audio Graph: centers around the AVAudioEngine framework. The "audio graph" refers to a graph of nodes that perform the necessary signal processing (playback, effects, mixing, etc) that sends the sound output to your audio device. The main classes here are the AudioGraph, Player, and Recorder. BufferManager does all the dirty work of scheduling audio buffers for Player.
2 - Playlist: CRUD for tracks. Also performs search/sort functions.
3 - PlaybackSequence: Contains logic for repeat and shuffle. Decides which track is going to play next.
As much as possible, code is accessed through protocols, not directly through concrete implementation classes, which is a fundamental OOP principle.
Example call chain: As an example, when the user changes the volume using the volume slider, control flows as follows:
AudioGraphViewController -> AudioGraphDelegate (through AudioGraphDelegateProtocol) -> AudioGraph (through AudioGraphProtocol) -> AVAudioPlayerNode.setVolume().
View -> Delegate -> Back end
Another example: When a track is removed from the playlist, the following happens:
PlaylistViewController -> PlaylistDelegate -> PlaylistMutatorDelegate -> Playlist.removeTracks() (through PlaylistCRUDProtocol)
Object Graph: Constructs/initializes all the delegate/back end objects necessary.
AppState: Class that encapsulates all persistent app state. When you exit the app, this object is persisted to a json file, and loaded back upon startup.
Preferences: User preferences. Uses the built-in system-provided UserDefaults mechanism.
Messaging: A very important part of the app is messaging, both synchronous and asynchronous. Often, one app component does its job and needs to tell other app components that it has done something, so that the other component may do something else in response. But, it has to do this without tight coupling between the two components. For instance, when a playlist track is removed from the playlist, if the removed track was playing, the playlist view needs to tell the playback controller to stop playing the track, so it will send out a message.
Another example - When the app is about to exit, the app delegate will send out an "exiting" message. If a recording is ongoing, the recorder unit will respond with "Wait ! Don't exit, let me prompt the user to save his recording".
The two main files that hold message definitions are SyncMessages.swift and AsyncMessages.swift.
IO and Utils: The IO and Utils source groups (folders within the project) contain a set of utility classes for performing disk IO and other functions like formatting strings, performing UI computations, concurrency, file system computations, etc.
MetadataSpecs: ID3 and ITunes metadata specifications which map format-specific tags like TLEN to user-friendly descriptions like "Duration"
Track lazy loading: One important thing to note is that track info is lazily loaded for optimal performance. When you add files to the playlist, no info whatsoever is read from the track. Then, other info (duration, artist, title, art) are read asynchronously, and the playlist updated. Finally, when you actually play the track, or prepare the track for playback (which is done eagerly), it's audio track is actually read. TrackIO is the class that handles the loading of track info.
Hope this helps !