Skip to content

Instantly share code, notes, and snippets.

@mntone
Last active December 12, 2021 03:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mntone/afc6908ae1b18d4d405997aaddb5e4d8 to your computer and use it in GitHub Desktop.
Save mntone/afc6908ae1b18d4d405997aaddb5e4d8 to your computer and use it in GitHub Desktop.
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