MacBook Air M1 Zhuyin Keyboard written with SwiftUI ~=300 LOC, < 4hrs
// | |
// VirtualKeyboard.swift | |
// MacBook Air M1 Zhuyin Keyboard | |
// Written with SwiftUI ~=300 LOC, < 4hrs | |
// Created by Ethan Huang on 2021/1/13. | |
// Twitter: @ethanhuang13 | |
import SwiftUI | |
struct VirtualKeyboard: View { | |
var body: some View { | |
VStack(spacing: .zero) { | |
HStack(spacing: .zero) { | |
speaker | |
keyboard | |
.padding(.horizontal, 15) | |
speaker | |
} | |
.padding(.horizontal, 15) | |
.padding(.vertical, 15) | |
trackPad | |
} | |
.padding(.vertical) | |
.background(Color(white: 0.6)) | |
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) | |
} | |
private var speaker: some View { | |
Color(white: 0.5) | |
.frame(width: defaultWidth * 0.6) | |
.padding(.vertical) | |
} | |
private var trackPad: some View { | |
HStack { // Touchpad | |
Color(white: 0.5) | |
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) | |
.frame(width: defaultWidth * 8.8, height: defaultHeight * 5) | |
} | |
} | |
private var keyboard: some View { | |
VStack(spacing: defaultWidth / 10) { | |
let allKeys: [[KeyCap.Config]] = [ | |
[.esc, | |
.fn(top: "", bottom: "F1"), | |
.fn(top: "", bottom: "F2"), | |
.fn(top: "", bottom: "F3"), | |
.fn(top: "", bottom: "F4"), | |
.fn(top: "", bottom: "F5"), | |
.fn(top: "", bottom: "F6"), | |
.fn(top: "", bottom: "F7"), | |
.fn(top: "", bottom: "F8"), | |
.fn(top: "", bottom: "F9"), | |
.fn(top: "", bottom: "F10"), | |
.fn(top: "", bottom: "F11"), | |
.fn(top: "", bottom: "F12"), | |
.touchId], | |
[.topBottom(top: "~", bottom: "."), | |
.grid(["!", "1", "ㄅ", ""]), | |
.grid(["@", "2", "ㄉ", ""]), | |
.grid(["#", "3", "ˇ", ""]), | |
.grid(["$", "4", "ˋ", ""]), | |
.grid(["%", "5", "ㄓ", ""]), | |
.grid(["^", "6", "ˊ", ""]), | |
.grid(["&", "7", "˙", ""]), | |
.grid(["*", "8", "ㄚ", ""]), | |
.grid(["(", "9", "ㄞ", ""]), | |
.grid([")", "0", "ㄢ", ""]), | |
.grid(["_", "-", "ㄦ", ""]), | |
.topBottom(top: "+", bottom: "="), | |
.backspace], | |
[.tab, | |
.grid(["", "Q", "ㄆ", ""]), | |
.grid(["", "W", "ㄊ", ""]), | |
.grid(["", "E", "ㄍ", ""]), | |
.grid(["", "R", "ㄐ", ""]), | |
.grid(["", "T", "ㄔ", ""]), | |
.grid(["", "Y", "ㄗ", ""]), | |
.grid(["", "U", "ㄧ", ""]), | |
.grid(["", "I", "ㄛ", ""]), | |
.grid(["", "O", "ㄟ", ""]), | |
.grid(["", "P", "ㄣ", ""]), | |
.grid(["『", "{", "「", "["]), | |
.grid(["』", "}", "」", "]"]), | |
.grid(["", "|", "、", "\\"])], | |
[.capsLock, | |
.grid(["", "A", "ㄇ", ""]), | |
.grid(["", "S", "ㄋ", ""]), | |
.grid(["", "D", "ㄎ", ""]), | |
.grid(["", "F", "ㄑ", ""]), | |
.grid(["", "G", "ㄕ", ""]), | |
.grid(["", "H", "ㄘ", ""]), | |
.grid(["", "J", "ㄨ", ""]), | |
.grid(["", "K", "ㄜ", ""]), | |
.grid(["", "L", "ㄠ", ""]), | |
.grid(["", ":", "ㄤ", ";"]), | |
.topBottom(top: "\"", bottom: "'"), | |
.return], | |
[.leftShift, | |
.grid(["", "Z", "ㄈ", ""]), | |
.grid(["", "X", "ㄌ", ""]), | |
.grid(["", "C", "ㄏ", ""]), | |
.grid(["", "V", "ㄒ", ""]), | |
.grid(["", "B", "ㄖ", ""]), | |
.grid(["", "N", "ㄙ", ""]), | |
.grid(["", "M", "ㄩ", ""]), | |
.grid([",", "<", "ㄝ", ","]), | |
.grid(["。", ">", "ㄡ", "."]), | |
.grid(["?", "", "ㄥ", "/"]), | |
.rightShift], | |
[.globe, | |
.control, | |
.leftOption, | |
.leftCommand, | |
.space, | |
.rightCommand, | |
.rightOption, | |
.directions] | |
] | |
ForEach(0 ..< allKeys.count) { index in | |
let keys = allKeys[index] | |
HStack { | |
ForEach(keys.map { AnyIdentifiable($0) }) { | |
KeyCap($0.value) | |
} | |
} | |
} | |
} | |
.padding(10) | |
.background(Color(white: 0.5)) | |
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) | |
} | |
} | |
private let fnHeight: CGFloat = 25 | |
private let defaultWidth: CGFloat = 50 | |
private let defaultHeight: CGFloat = 50 | |
struct AnyIdentifiable<T>: Identifiable { | |
let id = UUID() | |
let value: T | |
init(_ value: T) { | |
self.value = value | |
} | |
} | |
struct KeyCap: View { | |
enum Config { | |
case esc | |
case fn(top: String, bottom: String) | |
case touchId | |
case topBottom(top: String, bottom: String) | |
case grid([String]) | |
case backspace | |
case tab | |
case capsLock, `return` | |
case leftShift, rightShift | |
case globe, control, leftOption, leftCommand, space, rightCommand, rightOption | |
case directions | |
} | |
init(_ config: Config) { | |
self.config = config | |
} | |
let config: Config | |
var body: some View { | |
if case Config.directions = config { | |
key | |
} else { | |
key | |
.padding(5) | |
.background(Color.black) | |
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) | |
} | |
} | |
private struct Grid: View { | |
let keys: [String] | |
var body: some View { | |
let columns = [GridItem(.fixed(defaultWidth / 2), spacing: 5), | |
GridItem(.fixed(defaultWidth / 2), spacing: 5)] | |
LazyVGrid(columns: columns, spacing: 0, content: { | |
ForEach(keys.map { AnyIdentifiable($0) }) { | |
Text($0.value) | |
.padding(0) | |
} | |
}) | |
} | |
} | |
private struct TopBottom: View { | |
init(top: String, bottom: String, alignment: HorizontalAlignment = .center) { | |
self.top = top | |
self.bottom = bottom | |
self.alignment = alignment | |
} | |
let top: String | |
let bottom: String | |
let alignment: HorizontalAlignment | |
var body: some View { | |
VStack(alignment: alignment, spacing: 5) { | |
Text(top) | |
.padding(.horizontal, 3) | |
Text(bottom) | |
.padding(.horizontal, 3) | |
} | |
} | |
} | |
private struct Arrow: View { | |
let key: String | |
var body: some View { | |
Text(key) | |
.font(.caption) | |
.frame(width: defaultWidth * 1.15, height: defaultHeight / 1.7) | |
.background(Color.black) | |
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) | |
} | |
} | |
@ViewBuilder | |
var key: some View { | |
switch config { | |
case .esc: | |
HStack { | |
Text("esc") | |
.frame(width: defaultWidth, height: fnHeight, alignment: .leading) | |
} | |
case .fn(let top, let bottom): | |
VStack { | |
Text(top) | |
Text(bottom) | |
.font(.caption2) | |
} | |
.frame(width: defaultWidth * 1.083, height: fnHeight) | |
case .touchId: | |
Text("") | |
.frame(width: defaultWidth / 2, height: fnHeight) | |
case .topBottom(let top, let bottom): | |
TopBottom(top: top, bottom: bottom) | |
.frame(width: defaultWidth, height: defaultHeight) | |
.font(.title2) | |
.frame(width: defaultWidth, height: defaultHeight) | |
case .grid(let keys): | |
Grid(keys: keys) | |
.font(.title2) | |
.frame(width: defaultWidth, height: defaultHeight) | |
case .backspace: | |
TopBottom(top: "", bottom: "") | |
.frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .trailing) | |
case .tab: | |
TopBottom(top: "", bottom: "") | |
.frame(width: defaultWidth * 1.5, height: defaultHeight, alignment: .leading) | |
case .capsLock: | |
TopBottom(top: "•", bottom: "中/英", alignment: .leading) | |
.frame(width: defaultWidth * 2, height: defaultHeight, alignment: .leading) | |
case .return: | |
TopBottom(top: "", bottom: "") | |
.frame(width: defaultWidth * 1.9, height: defaultHeight, alignment: .trailing) | |
case .leftShift: | |
TopBottom(top: "", bottom: "") | |
.frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .leading) | |
case .rightShift: | |
TopBottom(top: "", bottom: "") | |
.frame(width: defaultWidth * 2.6, height: defaultHeight, alignment: .trailing) | |
case .globe: | |
Grid(keys: ["", "fn", "", ""]) | |
.font(.title3) | |
.frame(width: defaultWidth, height: defaultHeight) | |
case .control: | |
TopBottom(top: "", bottom: "control", alignment: .trailing) | |
.frame(width: defaultWidth, height: defaultHeight, alignment: .trailing) | |
case .leftOption: | |
TopBottom(top: "", bottom: "option", alignment: .trailing) | |
.frame(width: defaultWidth, height: defaultHeight, alignment: .trailing) | |
case .leftCommand: | |
TopBottom(top: "", bottom: "command", alignment: .trailing) | |
.frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .trailing) | |
case .space: | |
Color.clear | |
.frame(width: defaultWidth * 6.4, height: defaultHeight) | |
case .rightCommand: | |
TopBottom(top: "", bottom: "command", alignment: .trailing) | |
.frame(width: defaultWidth * 1.35, height: defaultHeight, alignment: .leading) | |
case .rightOption: | |
TopBottom(top: "", bottom: "option", alignment: .trailing) | |
.frame(width: defaultWidth, height: defaultHeight, alignment: .leading) | |
case .directions: | |
VStack(spacing: 0) { | |
HStack { | |
Arrow(key: "") | |
} | |
HStack(spacing: 8) { | |
Arrow(key: "") | |
Arrow(key: "") | |
Arrow(key: "") | |
} | |
} | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
VirtualKeyboard() | |
} | |
} |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Announcement tweet: https://twitter.com/ethanhuang13/status/1349944306954473472?s=21 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
How about add a screenshot?