MinimizableView
SwiftUI view that minimizes to the bottom of the screen similar to the mini-player in Apple Music or Spotify.
Install / Use
/learn @DominikButz/MinimizableViewREADME
MinimizableView (iOS 13+ / iPadOS)
MinimizableView is a simple SwiftUI view for iOS and iPadOS that can minimize like the mini-player in the Spotify or Apple Music app.
Breaking changes in version 2.0. See details below in the version history
Special thanks to Kavsoft (see here) - I used parts of their MiniPlayer content in the example. The framework is my own creation though.
Example project
This repo only contains the Swift package, no example code. Please download the example project here. You need to add the MinimizableView package either through cocoapods or the Swift Package Manager (see below - Installation).
Features
- Create your own content, background and compact view. The compact view is optional - in case you set it in the initializer, it will appear in the minimized state.
- By changing the setting properties of the MinimizableViewHandler, you can customize the following properties:
- minimizedHeight
- overrideHeight (in case you want to set a height different from the geometry size height)
- lateralMargin
- edgesIgnoringSafeAreas
Check out the examples for details.
Installation
Installation through the Swift Package Manager (SPM) or cocoapods is recommended.
SPM: Select your project (not the target) and then select the Swift Packages tab. Click + and type MinimizableView - SPM should find the package on github.
Cocoapods:
platform :ios, '14.0'
target '[project name]' do pod 'MinimizableView' end
Check out the version history below for the current version.
Make sure to import MinimizableView in every file where you use the MinimizableView or MinimizableViewHandler
import MinimizableView
Usage
Check out the following example. This repo only contains the Swift package, no example code. Please download the example project here.
<img src="gitResources/example01.gif" alt="example" width="320"/>Code example: Content View (your main view)
Simply attach the .minimizableView modifier to your main view, e.g. a TabView. To trigger presentation, dismissal, minimization and expansion, you need to call the respective functions of the minimizableViewHandler: present(), dismiss(), minimize() and expand(). It is advisable to call toggleExpansionState() on the minimizableViewHandler whenever you use a tapGesture to toggle the expansion state.
If you don't want a separate compact view, just pass an EmptyView into the compactView closure of the initialiser. The code in the body of MinimizableView checks if compactView is an EmptyView and in that case does not display it. if there is no compact view, the top of your content will be shown at the bottom of the screen in minimized state. Use the minimizableViewHandler as EnvironmentObject in your content view - e.g. to remove and insert certain subviews (or to change their opacity) once the minimized property changes (see the example below).
You also need to attach the minimizableViewHandler as environment object to the MinimizableView.
NEW in version 2.4: update the miniViewBottomMargin parameter dynamically as the height of your tab bar changes. This is achieved with the help of TabBarAccessor. The height can change depending on the device and resizing of the app window (e.g. with stage manager on iPad).
struct RootView: View {
@ObservedObject var miniHandler: MinimizableViewHandler = MinimizableViewHandler()
@State var selectedTabIndex: Int = 0
@State var miniViewBottomMargin: CGFloat = 0
@GestureState var dragOffset = CGSize.zero
@Namespace var namespace
var body: some View {
GeometryReader { proxy in
TabView(selection: self.$selectedTabIndex) {
Button(action: {
print(proxy.safeAreaInsets.bottom)
self.miniHandler.present()
}) { TranslucentTextButtonView(title: "Launch Minimizable View", foregroundColor: .green, backgroundColor: .green)}.disabled(self.miniHandler.isPresented)
.tabItem {
Image(systemName: "chevron.up.square.fill")
Text("Main View")
}.tag(0)
.background(TabBarAccessor { tabBar in // add to update the minimizedBottomMargin dynamically!
self.miniViewBottomMargin = tabBar.bounds.height - 1
})
Text("More stuff").tabItem {
Image(systemName: "dot.square.fill")
Text("2nd View")
}.tag(1)
ListView(availableWidth: proxy.size.width)
.tabItem {
Image(systemName: "square.split.2x1.fill")
Text("List View")
}.tag(2)
}.background(Color(.secondarySystemFill))
.statusBar(hidden: self.miniHandler.isPresented && self.miniHandler.isMinimized == false)
.minimizableView(content: {ContentExample(animationNamespaceId: self.namespace)},
compactView: {
EmptyView() // replace EmptyView() by CompactViewExample() to see the a different approach for the compact view
}, backgroundView: {
self.backgroundView()},
dragOffset: $dragOffset,
dragUpdating: { (value, state, transaction) in
state = value.translation
self.dragUpdated(value: value)
}, dragOnChanged: { (value) in
// add some custom logic if needed
},
dragOnEnded: { (value) in
self.dragOnEnded(value: value)
}, minimizedBottomMargin: self.miniViewBottomMargin, settings: MiniSettings(minimizedHeight: 80))
.environmentObject(self.miniHandler)
}
//
}
func backgroundView() -> some View {
VStack(spacing: 0){
BlurView(style: .systemChromeMaterial)
if self.miniHandler.isMinimized {
Divider()
}
}.cornerRadius(self.miniHandler.isMinimized ? 0 : 20)
.onTapGesture(perform: {
if self.miniHandler.isMinimized {
self.miniHandler.expand()
//alternatively, override the default animation. self.miniHandler.expand(animation: Animation)
}
})
}
func dragUpdated(value: DragGesture.Value) {
if self.miniHandler.isMinimized == false && value.translation.height > 0 { // expanded state
self.miniHandler.draggedOffsetY = value.translation.height // divide by a factor > 1 for more "inertia" if needed
} else if self.miniHandler.isMinimized && value.translation.height < 0 {// minimized state
self.miniHandler.draggedOffsetY = value.translation.height // divide by a factor > 1 for more "inertia" if needed
}
}
func dragOnEnded(value: DragGesture.Value) {
if self.miniHandler.isMinimized == false && value.translation.height > 90 {
self.miniHandler.minimize()
} else if self.miniHandler.isMinimized && value.translation.height < -60 {
self.miniHandler.expand()
}
withAnimation(.spring()) {
self.miniHandler.draggedOffsetY = 0
}
}
}
Change log
Version 2.4.2
Updated default value of mini settings minimumDragDistance to 1 in order to prevent unresponsive subviews in the content view, e.g. List scrolling and dragging a slider.
Version 2.4.1
updating readme after merge with pull request crossplatform project friendliness #9. Excluding macOS conflicts through pre-processor conditionals limiting package usage to iOS / iPadOS.
Version 2.4
- initializer update: removed geometry parameter
- presentation is now done properly with a move-transition
- simplified calculation of position and offset of mini view
- added TabBarAccessor UIViewRepresentable struct. see the example project and updated readme on how to use this. The bottom line is that it helps to update minimizedBottomMargin dynamically in reaction to a change of the tab bar view height.
Version 2.3.3
Fixes presentation and dismiss transition bug that would move the background out of the view separately from the content.
Version 2.3.2
Added minimumDragDistance to settings. If your content view contains a List, make sure to set this value > 0 (usually between 10 and 30 is a suitable value) - this will make sure the List is scrollable.
Version 2.3.1
Bug fix: If the user drags the mini view up or down and simultaneously does a pan gestu
Related Skills
node-connect
351.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
110.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
351.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
351.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
