iOS Transparent Background Widget

Wing CHAN
5 min readNov 7, 2021

--

中文版
https://wingpage.net/content/iOS/WidgetsKit%20Transparent%20background.html

Transparent Background Widget

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

  1. Long press the main screen to enter edit mode
  2. 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

  1. background picture
  2. Home Widget relative location
  3. each spacing
  4. 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

  1. safeAreaInsetTop
  2. WidgetPosition
  3. 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.")
}
}

--

--