中文版
https://wingpage.net/content/iOS/WidgetsKit%20Transparent%20background.html
The Home widget does not support transparent background, so you have to set the widget background to be the same as the screen background to make the effect transparent.
Objective:
Calculate the position of the widget in the background image and crop it.
Ideas
Screenshot of the main screen, use it to crop the background of the widget
- Long press the main screen to enter edit mode
- Swipe to the last page and take a screenshot
Calculate the position and size of the relative background and crop it to the background of the widget
Implement
First of all, you should know the following information
- background picture
- Home Widget relative location
- each spacing
- widget size
1. Choose a background image
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel: ContentViewModel = ContentViewModel()
var body: some View {
ZStack(alignment: .bottom) {
Image(uiImage: (self.viewModel.backgroundImage == nil ? UIImage() : UIImage(data: self.viewModel.backgroundImage!))!)
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.edgesIgnoringSafeArea(.all)
VStack {
Button(action: self.viewModel.onClickSelectImage) {
HStack {
Image(systemName: "photo")
.font(.system(size: 20))
Text("Photo library")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 50)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(20)
.padding()
}
}
}
.sheet(isPresented: self.$viewModel.isShowPhotoLibrary) {
// https://www.appcoda.com.tw/swiftui-camera-photo-library/
ImagePicker(selectedImage: viewModel.onSelecedtImage, sourceType: .photoLibrary)
}
.statusBar(hidden: self.viewModel.backgroundImage == nil ? false : true)
}
}
Save the image as a `Data` in UserDefaults, and get the image from UserDefaults in Widget extension
import Foundation
import SwiftUI
class ContentViewModel: ObservableObject {
let userDefaults = UserDefaults(suiteName: "groupId");
@Published var backgroundImage: Data? {
didSet {
userDefaults?.set(backgroundImage, forKey: "userDefaultsSharedBackgroundImage")
}
}
@Published var isShowPhotoLibrary = false
func onClickSelectImage(){
self.isShowPhotoLibrary = true
}
func onSelecedtImage(image: UIImage) {
if let data = image.jpegData(compressionQuality: 0.8) {
backgroundImage = data
}
}
}
2. Widget position
enum WidgetPosition: String, CaseIterable, Codable, Identifiable {
case top
case leftTop
case rightTop
case middle
case leftMiddle
case rightMiddle
case bottom
case leftBottom
case rightBottom
static func availablePositions(_ widgetFamily: WidgetFamily) -> [WidgetPosition] {
switch widgetFamily {
case .systemSmall:
return [leftTop, rightTop, leftMiddle, rightMiddle, leftBottom, rightBottom]
case .systemMedium:
return [top, middle, bottom]
case .systemLarge:
return [top, bottom]
case .systemExtraLarge:
fatalError("Not yet implemented")
@unknown default:
fatalError("Not yet implemented")
}
}
var id: WidgetPosition { self }
}
Also save to UserDefault
// ContentViewModel.swift
userDefaults?.set(WidgetPosition.leftTop.rawValue, forKey: "userDefaultsWidgetPosition")
3. Get the height of the status bar
Since you can’t get safeAreaInsets in Widget Extension, you have to get it in App first and then store it in UserDefault, then Widget Extension will get safeAreaInsets from UserDefault.
let window = UIApplication.shared.windows.first
if let safeAreaPadding = window?.safeAreaInsets.top {
userDefaults?.set(Double(safeAreaPadding), forKey: "userDefaultsSharedSafeAreaInsetTop")
}
4. Cropping the background
extension UIImage
{
// iPhone 13 Pro
// useful size resource: https://www.screensizes.app/
func cropToWidgetSize(
safeAreaInsetTop: Double,
widgetSize: CGSize,
widgetPosition: WidgetPosition
)-> UIImage{
// use for calculate pt to px
// iPhone 13 Pro: 390 x 844 -> 1170 x 2532
let scale = UIScreen.main.scale
// status Bar height
let statusBarHeight = safeAreaInsetTop;
let topMargin = 30.0;
let leftMargin = 25.0;
let verticalMargin: CGFloat = 23;
let horizontalMargin: CGFloat = 38.2;
var startPoint: CGPoint = CGPoint(x: leftMargin, y: (topMargin + statusBarHeight));
switch widgetPosition {
case .top:
startPoint.x += 0;
case .leftTop:
startPoint.x += 0;
case .rightTop:
startPoint.x += (widgetSize.width + verticalMargin);
case .middle, .leftMiddle:
startPoint.x += 0;
startPoint.y += (widgetSize.height + horizontalMargin);
break
case .rightMiddle:
startPoint.x += (widgetSize.width + verticalMargin);
startPoint.y += (widgetSize.height + horizontalMargin);
break
case .bottom, .leftBottom:
startPoint.x += 0;
startPoint.y += (widgetSize.height + horizontalMargin) * 2;
break
case .rightBottom:
startPoint.x += (widgetSize.width + verticalMargin);
startPoint.y += (widgetSize.height + horizontalMargin) * 2;
break
}
let cropArea = CGRect(
x: startPoint.x * scale,
y: startPoint.y * scale,
width: widgetSize.width * scale,
height: widgetSize.height * scale
)
let cropedImage = self.cropImage(toRect: cropArea)
return cropedImage;
}
func cropImage( toRect rect:CGRect) -> UIImage{
let imageRef:CGImage = self.cgImage!.cropping(to: rect)!
let croppedImage:UIImage = UIImage(cgImage:imageRef)
return croppedImage
}
}
5. Widget Extension
Get the following data from UserDefaults, and crop it
- safeAreaInsetTop
- WidgetPosition
- backgroundImage
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), image: UIImage())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), image: UIImage())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
if let userDefaults = UserDefaults(suiteName: groupId) {
let safeAreaInsetTop = userDefaults.double(forKey: userDefaultsSharedSafeAreaInsetTop)
var widgetPosition : WidgetPosition = WidgetPosition.leftTop
if let widgetPositionRawValue = userDefaults.object(forKey: "widgetPosition") as? String{
widgetPosition = WidgetPosition(rawValue: widgetPositionRawValue)!
}
var image: UIImage = UIImage()
if let backgroundImage = userDefaults.data(forKey: userDefaultsSharedBackgroundImage) {
// 裁剪
image = UIImage(data:backgroundImage)!.cropToWidgetSize(
safeAreaInsetTop: safeAreaInsetTop,
widgetSize: context.displaySize,
widgetPosition: widgetPosition
)
}
entries.append(
SimpleEntry(date: Date(), image: image)
)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let image: UIImage
}
struct HomeWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack {
Image(uiImage: entry.image)
.resizable()
.scaledToFill()
Image("widgetImage")
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
}
}
@main
struct HomeWidget: Widget {
let kind: String = "HomeWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
HomeWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}