Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Copyright (C) 2021 mntone. All right reserived. This source code is under MIT license.
import SwiftUI
struct BorderedProminentButtonModifier: ViewModifier {
@Environment(\.deviceMetrics)
private var deviceMetrics: WatchDeviceMetrics
func body(content: Content) -> some View {
if #available(watchOS 8.0, *) {
content.tint(.accentColor)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: deviceMetrics.defaultControlCornerRadius))
} else {
content.buttonStyle(.borderedProminentCompat)
}
}
}
private var _borderedProminentButtonModifier: BorderedProminentButtonModifier = BorderedProminentButtonModifier()
extension View {
func borderedProminentButtonStyleCompat() -> some View {
modifier(_borderedProminentButtonModifier)
}
}
import SwiftUI
@available(watchOS, introduced: 7.0, deprecated: 8.0)
struct BorderedProminentButtonStyleCompatible: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(configuration: configuration)
}
private struct Button: View {
@Environment(\.deviceMetrics)
private var deviceMetrics: WatchDeviceMetrics
let configuration: ButtonStyle.Configuration
var body: some View {
configuration.label
.frame(maxWidth: .infinity, minHeight: deviceMetrics.smallControlHeight)
.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: deviceMetrics.defaultControlCornerRadius, style: .continuous))
.opacity(configuration.isPressed ? 0.8 : 1.0)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
}
}
}
@available(watchOS, introduced: 7.0, deprecated: 8.0)
private var _borderedProminentCompat: BorderedProminentButtonStyleCompatible = BorderedProminentButtonStyleCompatible()
@available(watchOS, introduced: 7.0, deprecated: 8.0)
extension ButtonStyle where Self == BorderedProminentButtonStyleCompatible {
static var borderedProminentCompat: BorderedProminentButtonStyleCompatible {
_borderedProminentCompat
}
}
import SwiftUI
struct EdgeCircularButtonStyle: ButtonStyle {
private let position: Alignment
init(_ position: Alignment = .center) {
self.position = position
}
func makeBody(configuration: Self.Configuration) -> some View {
EdgeCircularButton(configuration: configuration, position: position)
}
private struct EdgeCircularButton: View {
@Environment(\.deviceMetrics)
private var deviceMetrics: WatchDeviceMetrics
@Environment(\.isEnabled)
private var isEnabled: Bool
let configuration: ButtonStyle.Configuration
let position: Alignment
var body: some View {
configuration.label
.imageScale(.medium)
.frame(width: deviceMetrics.edgeCircularButtonHeight, height: deviceMetrics.edgeCircularButtonHeight)
.background(Color.white.opacity(isEnabled ? 0.85 : 0.5))
.clipShape(Circle())
.shadow(color: .black.opacity(0.5), radius: 1)
.padding(getMargin(deviceMetrics.bezelType, position: position))
.contentShape(Rectangle())
.opacity(configuration.isPressed ? 0.8 : 1.0)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
}
private func getMargin(_ bezel: BezelType, position: Alignment) -> EdgeInsets {
switch bezel {
case .square:
switch position {
case .leading:
return EdgeInsets(top: 6, leading: 2, bottom: 6, trailing: 6)
case .trailing:
return EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 2)
case .top:
return EdgeInsets(top: 2, leading: 6, bottom: 6, trailing: 6)
case .bottom:
return EdgeInsets(top: 6, leading: 6, bottom: 2, trailing: 6)
case .topLeading:
return EdgeInsets(top: 2, leading: 2, bottom: 6, trailing: 6)
case .topTrailing:
return EdgeInsets(top: 2, leading: 6, bottom: 6, trailing: 2)
case .bottomLeading:
return EdgeInsets(top: 6, leading: 2, bottom: 2, trailing: 6)
case .bottomTrailing:
return EdgeInsets(top: 6, leading: 6, bottom: 2, trailing: 2)
default:
return EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)
}
case .round:
let c = deviceMetrics.minimumLayoutHorizontalMargin
switch position {
case .leading:
return EdgeInsets(top: c, leading: c, bottom: c, trailing: 6)
case .trailing:
return EdgeInsets(top: c, leading: 6, bottom: c, trailing: c)
case .top:
return EdgeInsets(top: c, leading: c, bottom: 6, trailing: c)
case .bottom:
return EdgeInsets(top: 6, leading: c, bottom: c, trailing: c)
case .topLeading:
return EdgeInsets(top: c, leading: c, bottom: 6, trailing: 6)
case .topTrailing:
return EdgeInsets(top: c, leading: 6, bottom: 6, trailing: c)
case .bottomLeading:
return EdgeInsets(top: 6, leading: c, bottom: c, trailing: 6)
case .bottomTrailing:
return EdgeInsets(top: 6, leading: 6, bottom: c, trailing: c)
default:
return EdgeInsets(top: c, leading: c, bottom: c, trailing: c)
}
}
}
}
}
extension ButtonStyle where Self == EdgeCircularButtonStyle {
static var edgeCircular: EdgeCircularButtonStyle {
EdgeCircularButtonStyle()
}
static func edgeCircular(_ position: Alignment) -> EdgeCircularButtonStyle {
EdgeCircularButtonStyle(position)
}
}
import CoreGraphics
import Foundation
import SwiftUI
import WatchKit
private let _case38Size = CGSize(width: 136, height: 170)
private let _case42Size = CGSize(width: 156, height: 195)
private let _case40Size = CGSize(width: 162, height: 197)
private let _case44Size = CGSize(width: 184, height: 224)
private let _case41Size = CGSize(width: 176, height: 215)
private let _case45Size = CGSize(width: 198, height: 242)
public enum WatchDevice: Int8, CaseIterable {
case unknown = 0
case case38 = 38
case case40 = 40
case case41 = 41
case case42 = 42
case case44 = 44
case case45 = 45
}
public enum BezelType {
case square
case round(RoundedBezelSize)
}
public enum RoundedBezelSize {
case wide
case narrow
}
public protocol WatchDeviceMetrics {
var device: WatchDevice { get }
var defaultControlCornerRadius: Double { get }
var smallControlHeight: Double { get }
var minimumLayoutHorizontalMargin: Double { get }
var bezelType: BezelType { get }
var imageScale: Double { get }
var edgeCircularButtonHeight: CGFloat { get }
}
public func getWatchDeviceMetrics(from size: CGSize) -> WatchDeviceMetrics {
if #available(watchOS 8.0, *) {
switch size {
case _case41Size: return Case41WatchDeviceMetrics()
case _case45Size: return Case45WatchDeviceMetrics()
case _case40Size: return Case40WatchDeviceMetrics()
case _case44Size: return Case44WatchDeviceMetrics()
case _case38Size: return Case38WatchDeviceMetrics()
case _case42Size: return Case42WatchDeviceMetrics()
default: return EmptyWatchDeviceMetrics()
}
} else {
switch size {
case _case40Size: return Case40WatchDeviceMetrics()
case _case44Size: return Case44WatchDeviceMetrics()
case _case38Size: return Case38WatchDeviceMetrics()
case _case42Size: return Case42WatchDeviceMetrics()
default: return EmptyWatchDeviceMetrics()
}
}
}
public func getWatchDeviceMetrics() -> WatchDeviceMetrics {
let size = WKInterfaceDevice.current().screenBounds.size
return getWatchDeviceMetrics(from: size)
}
// MARK: - each device metrics
private struct EmptyWatchDeviceMetrics: WatchDeviceMetrics {
var device: WatchDevice { fatalError() }
var defaultControlCornerRadius: Double { fatalError() }
var smallControlHeight: Double { fatalError() }
var minimumLayoutHorizontalMargin: Double { fatalError() }
var bezelType: BezelType { fatalError() }
var imageScale: Double { fatalError() }
var edgeCircularButtonHeight: CGFloat { fatalError() }
}
private struct Case38WatchDeviceMetrics: WatchDeviceMetrics {
let device: WatchDevice = .case38
let defaultControlCornerRadius: Double = 8
let smallControlHeight: Double = 32
let minimumLayoutHorizontalMargin: Double = 1
let bezelType: BezelType = .square
let imageScale: Double = 0.9
let edgeCircularButtonHeight: CGFloat = 27 // 0.5 * round(2 * imageScale * 30)
}
private struct Case42WatchDeviceMetrics: WatchDeviceMetrics {
let device: WatchDevice = .case42
let defaultControlCornerRadius: Double = 8
let smallControlHeight: Double = 32
let minimumLayoutHorizontalMargin: Double = 1
let bezelType: BezelType = .square
let imageScale: Double = 1
let edgeCircularButtonHeight: CGFloat = 30 // 0.5 * round(2 * imageScale * 30)
}
private struct Case40WatchDeviceMetrics: WatchDeviceMetrics {
let device: WatchDevice = .case40
let defaultControlCornerRadius: Double = 22
let smallControlHeight: Double = 40
let minimumLayoutHorizontalMargin: Double = 8.5
let bezelType: BezelType = .round(.wide)
let imageScale: Double = 1
let edgeCircularButtonHeight: CGFloat = 30 // 0.5 * round(2 * imageScale * 30)
}
private struct Case44WatchDeviceMetrics: WatchDeviceMetrics {
let device: WatchDevice = .case44
let defaultControlCornerRadius: Double = 22
let smallControlHeight: Double = 44
let minimumLayoutHorizontalMargin: Double = 9.5
let bezelType: BezelType = .round(.wide)
let imageScale: Double = 1.1
let edgeCircularButtonHeight: CGFloat = 33 // 0.5 * round(2 * imageScale * 30)
}
private struct Case41WatchDeviceMetrics: WatchDeviceMetrics {
let device: WatchDevice = .case41
let defaultControlCornerRadius: Double = 22
let smallControlHeight: Double = 40
let minimumLayoutHorizontalMargin: Double = 11
let bezelType: BezelType = .round(.narrow)
let imageScale: Double = 1.06
let edgeCircularButtonHeight: CGFloat = 32 // 0.5 * round(2 * imageScale * 30)
}
private struct Case45WatchDeviceMetrics: WatchDeviceMetrics {
let device: WatchDevice = .case45
let defaultControlCornerRadius: Double = 22
let smallControlHeight: Double = 50
let minimumLayoutHorizontalMargin: Double = 12
let bezelType: BezelType = .round(.narrow)
let imageScale: Double = 1.19
let edgeCircularButtonHeight: CGFloat = 35.5 // 0.5 * round(2 * imageScale * 30)
}
import SwiftUI
struct WatchDeviceMetricsKey: EnvironmentKey {
static var defaultValue: WatchDeviceMetrics {
EmptyWatchDeviceMetrics()
}
}
extension EnvironmentValues {
var deviceMetrics: WatchDeviceMetrics {
get { self[WatchDeviceMetricsKey.self] }
set { self[WatchDeviceMetricsKey.self] = newValue }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment