Some of the most common recurring questions in the Django Discord revolve around static files, why they’re not loading when they should, or how to configure them and what should be responsible for serving them. I thought I’d write up a bit of an introduction to static files in Django, which will hopefully be a little more approachable to beginners than the official docs (as excellent as they are), and serve as a starting point for diagnosing problems with them.
Static files are files served as part of your Django application whose contents do not change from user to user or request to request—they are not dynamic. By far the most common examples are the stylesheets (CSS), Javascript, and images that are required for the application to look and function correctly in the browser, but they could just as easily be other things—JSON files of data that doesn’t change, for example.
Static files are not any files that your users upload, such as profile pictures, or other content. Those are referred to by Django as media files, and are handled differently.
There are three key pieces of information to know about Django’s static files:
These are three distinct, but interrelated, pieces of the puzzle, and they each have different settings to adjust or define their behaviour in settings.py
.
By default, Django will look for your static files in a directory named static
inside each app directory in INSTALLED_APPS
, and without any additional configuration this is the only place it will search. For many, that’s enough.
Others may wish to store some static files outside any one particular app, because they may be relevant globally to the project and there isn’t a single app that fits them best. This can be done by creating a new directory somewhere (often, named static
at the same level as manage.py
), and adding a new setting to settings.py
:
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
The STATICFILES_DIRS
setting tells Django to look for static files in those additional directories, as well as within each individual app directory.
The single setting, STATIC_URL
, defines the URL prefix that Django expects the static files to be available under when the browser requests them. This is entirely independent from their actual location on the file system as discussed above.
Django suggests a perfectly reasonable default for this: "static/"
. It must end in a slash.
When running your application, Django will expect the static files to be available under that URL. For example, a file in a directory my_app/static/css/styles.css
would correspond to a URL of /static/css/styles.css
.
When referencing static files in templates, such as using a link
or script
tag to include CSS or JavaScript in HTML, Django provides a template tag in the static
library to automatically convert a relative static file name into its full URL path. For example, to include the above CSS file:
{% load static %}
…
<link href="{% static "css/main.css" %}" rel="stylesheet">
This would result in the following HTML output:
<link href="/static/css/main.css" rel="stylesheet">
And what’s this third key point I mentioned above, the single static directory?
The answer to this differs depending on how you’re running Django; in development or production.
During development, it’s acceptable for Django to serve the static files itself. Normally, this job would be handed off to a web server or proxy, as there is no benefit to spinning up the full Django application process just to chuck some static CSS back to the browser, but during development it is really not worth having to set that all up.
When running Django with the development server (manage.py runserver
) and DEBUG=True
, Django will automatically add a new URL path to your URL patterns that matches the value of STATIC_URL
, and search the various directories mentioned above for matching files to serve whenever a request comes in. This means that by only setting a STATIC_URL
to something sensible, Django will automatically be able to serve the CSS, JavaScript, and images stored in any static
folder inside each INSTALLED_APPS
directory, and any folders pointed to by STATICFILES_DIRS
. That’s usually enough for development purposes.
However, as mentioned above, it’s inefficient to have Django serve these files itself when deployed in a production environment. Instead, that responsibility is usually handed off to a web server or proxy that sits in front of Django, such as nginx. So how does this proxy know where to find the static files?
The answer is in the STATIC_ROOT
setting. This should be pointed to a directory on the file system that, once deployed, both the Django application and the proxy can access.
What should be in that directory? Nothing, to start with. Part of the deployment process should be to run manage.py collectstatic
, a built-in management command which runs round all the various folders the static files live in (see the first point above) and copies them into the STATIC_ROOT
directory, ready for the proxy to efficiently serve them.
The final part of the puzzle is to correctly configure the proxy so that requests for any path matching the prefix defined in STATIC_URL
map onto the folder defined in STATIC_ROOT
. This way, once collectstatic
has been run, all the discrete parts of the application point together at a single, efficiently-served directory of static files.
Follow these steps to help diagnose why styles may not be loading, images not showing up, or any other static files not appearing correctly in the browser.
static
under one of your INSTALLED_APPS
, or in a directory pointed at by STATICFILES_DIRS
.STATIC_URL
is configured and ends in a /
.{% static "relative/path/to/file" %}
template tag wherever you reference static files in your templates. runserver
, make sure DEBUG=True
. Restart the runserver
to be sure it picks up any changes you may have made. STATIC_ROOT
points to a directory on the file system, you have run collectstatic
, and whatever proxy that fronts Django is properly configured to point STATIC_URL
at the STATIC_ROOT
directory. Note also that browsers will cache static files, so a forced refresh can help debug. This is usually done with Ctrl-F5
, but varies from browser to browser.
Hopefully this should provide a basic introduction to static files in Django, with enough information to get you up and running and help diagnose when static files aren’t working as you expect. For further reading, I suggest looking at the following resources:
STATICFILES_FINDERS
. staticfiles
app.. If this app is not included in INSTALLED_APPS
, none of the above will work.LEGO’s got some really strong Creator 3-in-1 sets out this year, and 31154 Forest Animals: Red Fox is one of the best. You can build a fox, an owl, or a squirrel, and all three models are truly excellent.
31154 Forest Animals: Red Fox doesn’t disappoint; all three models are fantastic, were a joy to build, and look great on display. Each manages to capture the unique characteristics of the woodland creatures, and the articulation makes them quite satisfying to pose in the way that you want.
Once again LEGO are coming up with some very creative 3-in-1 sets for their first wave of 2024:
I’m a big fan of the 3-in-1 range. Lately LEGO have been knocking it out of the park with so many of these sets, and this one held great promise. I love the colour scheme, and the roller skate is a brilliant build. It’s a decent size, feels solid in the hand, and although it’s not a vehicle, pushing it around is quite satisfying. It’s just a shame there’s only one—a pair would have been fantastic!
In my previous post, Anatomy of a Widget, I outlined my basic understanding of building a simple widget in Xcode. These were the most trivial widgets possible: they provided no options, and certainly no interactivity as introduced with iOS 17. In this post, I’m going to write up my (also limited) understanding of the parts that need to be added to provide configurable options within the widget—in other words, those the user sees when they long press on a widget and hit “Edit”.
The previous post introduced the concept of the the timeline entry, the timeline provider, the widget’s view, the widget definition, and the widget bundle. We’ll be adding one more, the configuration intent, and tying it in to the rest.
The same major caveat as before continues to apply: I still do not fully understand widgets, or really know how to build them properly. Nor do I understand interactive widgets that come in with iOS 17. But, hopefully, my knowledge will increase and I can update these posts as I learn more! Please do not hesitate to let me know about anything I write here that’s misleading or factually incorrect.
iOS apps use what Apple refers to as “intents” to tell other parts of the system what the app can do—such as Siri, or Shortcuts. The same mechanism is used by widgets to define what options are available, and we do this using a struct conforming to WidgetConfigurationIntent
. This struct needs to hold all the parameters available to the user in the widget’s edit menu. For example, a very simple widget intent could look like the following:
import WidgetKit
import AppIntents
struct MyConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Favourite Emoji", default: "😃")
var favouriteEmoji: String
}
This is about as basic as it gets, providing a single string parameter that will be exposed in the edit menu via a text field, with a sensible default. I’ll go through some other parameter types available later.
For now, we need to propagate the configuration intent throughout the rest of the widget’s stack.
The timeline entry is responsible for holding all the information the widget’s view needs to render for a given point in time. We need to update this to also hold the configuration intent. I will be using the example code from the previous post, and adding/amending it as necessary:
struct MyWidgetEntry: TimelineEntry {
let date: Date
let text: String
let configuration: MyConfigurationIntent
}
Because the timeline entries are created by the timeline provider, we need to update this to include the configuration intent too. This time, we also need to change from conforming to the basic TimelineProvider
protocol to the mode advanced AppIntentTimelineProvider
protocol, which also necessitates changing the method signatures of the three methods, placeholder
, getSnapshot
, and getTimeline
.
struct MyTimelineProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> MyWidgetEntry {
MyWidgetEntry(date: Date(), configuration: MyConfigurationIntent())
}
func snapshot(for configuration: MyConfigurationIntent, in context: Context) async -> MyWidgetEntry {
MyWidgetEntry(date: Date(), configuration: configuration)
}
func timeline(for configuration: MyConfigurationIntent, in context: Context) async -> Timeline<MyWidgetEntry> {
var entries: [MyWidgetEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = MyWidgetEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
return Timeline(entries: entries, policy: .atEnd)
}
}
For the placeholder
method, the timeline provider isn’t handed a configuration, so it has to create one to pass to the timeline entry. For the other two methods, however, the first argument passed is a MyConfigurationIntent
struct representing the options the user has selected in the widget edit view, and we can pass this directly to the timeline entry.
The only thing left to do is update the widget’s View to make use of the options provided by the configuration intent. In this case, we can use the favouriteEmoji
parameter, as it’s the only one provided by our very simple intent:
struct MyWidgetView : View {
var entry: MyWidgetEntry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text(entry.text)
Text("Favourite Emoji:")
Text(entry.configuration.favouriteEmoji)
}
}
}
And that’s it! The widget now allows the user to customise its view by presenting an edit menu with a bunch of parameters. The user-chosen values for these parameters are passed into the View as a Configuration Intent parameter via the timeline entry, and the View can make use of them as it wishes.
Above, we saw a very simple configuration intent parameter of a string:
@Parameter(title: "Favourite Emoji", default: "😃")
var favouriteEmoji: String
The title
is the name of the parameter as shown to the user in the edit view of the widget, and the default you provide is what the parameter is set to when the user hasn’t edited the widget and entered something else. You can view the WidgetConfigurationIntent
documentation for more options that are available, such as how to control the order the parameters appear in the widget edit view or define those which depend on others.
Adding other type of data is easy, such as asking for an integer:
@Parameter(title: "Age", default: 18)
var age: Int
Or a boolean:
@Parameter(title: "Show Background", default: false)
var showBackground: Bool
You can also present the user with a choice of options by conforming to DynamicOptionsProvider
:
struct IntegerOptionsProvider: DynamicOptionsProvider {
let count: Int
let defaultInteger: Int
func results() async throws -> [Int] {
Array(0...count)
}
func defaultResult() async -> Int? {
defaultInteger
}
}
...
@Parameter(title: "Hour", optionsProvider: IntegerOptionsProvider(count: 24, defaultInteger: 16))
var hour: Int
More complicated data types can be represented by conforming to various protocols, such as AppEnum
to provide users with a choice based on an enum:
enum Weekday: Int, AppEnum {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Weekday"
case Sunday = 1
case Monday = 2
case Tuesday = 3
case Wednesday = 4
case Thursday = 5
case Friday = 6
case Saturday = 7
static var caseDisplayRepresentations: [Weekday: DisplayRepresentation] = [
.Sunday: "Sunday",
.Monday: "Monday",
.Tuesday: "Tuesday",
.Wednesday: "Wednesday",
.Thursday: "Thursday",
.Friday: "Friday",
.Saturday: "Saturday"
]
}
...
@Parameter(title: "Weekday", default: .Friday)
var weekday: Weekday
Or by conforming an struct to AppEntity
and the associated EntityQuery
, you can add support for arbitrary data types, which is the most powerful but complicated option, such as this example for adding a time zone choice to the widget:
struct TimeZoneQuery: EntityStringQuery {
private func convertToWidgetTimeZone(identifiers: [String]) -> [WidgetTimeZone] {
identifiers.compactMap { TimeZone(identifier: $0) }.map { WidgetTimeZone(timezone: $0) }
}
func entities(matching string: String) async throws -> [WidgetTimeZone] {
return convertToWidgetTimeZone(identifiers: TimeZone.knownTimeZoneIdentifiers.filter { $0.localizedStandardContains(string) })
}
func entities(for identifiers: [String]) async throws -> [WidgetTimeZone] {
return convertToWidgetTimeZone(identifiers: TimeZone.knownTimeZoneIdentifiers.filter { identifiers.contains($0) })
}
func suggestedEntities() async throws -> [WidgetTimeZone] {
return convertToWidgetTimeZone(identifiers: TimeZone.knownTimeZoneIdentifiers)
}
}
struct WidgetTimeZone: Equatable, Hashable, AppEntity {
typealias DefaultQuery = TimeZoneQuery
static var defaultQuery: TimeZoneQuery = TimeZoneQuery()
static var typeDisplayName: LocalizedStringResource = LocalizedStringResource("TimeZone", defaultValue: "TimeZone")
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(stringLiteral: "TimeZone")
}
public var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: .init(stringLiteral: id))
}
var id: String { timezone.identifier }
var timezone: TimeZone
}
...
@Parameter(title: "Time Zone")
var timeZone: WidgetTimeZone?
An extension of the Configuration Intent protocol is also what powers the interactive widgets available in iOS 17. Hopefully, once I’ve figured those out a little more, a future post will cover the basics of them too!
I have long been a little confused by how widgets work, from a development perspective, in iOS apps. There are a number of moving parts that all have to work together just so to make the widget appear how you want, with the data you want, when you want. This post is my attempt to break it down into each part, in the order they need to be defined so the app still compiles after each step, with my understanding of what they’re for and what they do.
The major caveat here is: I still do not understand widgets, or really know how to build them properly. Nor do I understand interactive widgets that come in with iOS 17. But, hopefully, my knowledge will increase and I can update this post as I learn more! Please do not hesitate to let me know about anything I write here that’s misleading or factually incorrect.
Widgets are a series of static SwiftUI views, rendered on a timeline into the future. When the system reaches the end of the timeline, or at some point determined by your app or widget configuration, the app extension is asked for another timeline to render.
Each item in this timeline is a timeline entry, which is simply a struct conforming to TimelineEntry
. This struct needs to hold all the data your widget needs to know in order to render correctly. The date
property is mandatory (specified by the TimelineEntry
protocol), but all other properties are up to you. For example, a widget that simply renders some text may need a timeline entry such as the following:
struct MyWidgetEntry: TimelineEntry {
let date: Date
let text: String
}
This is the part of the widget that is responsible for providing each timeline when iOS asks for one, and needs to be a struct conforming to TimelineProvider
. Apple’s documentation is pretty good here. There are three required methods that need to be implemented:
placeholder
method must return, as quickly as possible, a single timeline entry for use in placeholder views (such as when the user taps the your app in the Add Widget gallery).getSnapshot
method also needs to provide a single timeline entry, but gets a bit more time to fetch real data, and can be used to make the widget previews in the gallery more representative of the actual widget once added.getTimeline
method must provide a Timeline
object with a list of dated entries stretching into the future, and a policy of when the timeline should be refreshed.struct MyTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> MyWidgetEntry {
WidgetEntry(date: Date(), text: "Placeholder")
}
func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> ()) {
let entry = MyWidgetEntry(date: Date(), text: "Snapshot")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> ()) {
var entries: [MyWidgetEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = MyWidgetEntry(date: entryDate, emoji: "In a timeline! \(hourOffset)")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
The most common refresh policy is .atEnd
, which will instruct iOS to ask for a new timeline once this one is complete. The widget will be rendered with each timeline entry at its specified date.
This is the core of the display of the widget, a SwiftUI view that takes the timeline entry as a parameter and renders the data as necessary. It doesn’t have to be a separate View
(it could be rendered as part of the widget itself, see below), but it’s much neater this way.
struct MyWidgetView : View {
var entry: MyWidgetEntry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text(entry.text)
}
}
}
There’s nothing magic here.
Each widget is a struct that conforms to Widget
, which looks similar to a SwiftUI View
with a couple of extra options:
kind
String constant, with a unique (to the app) identifier for the type of widget.WidgetConfiguration
object from body
, and provide the configurationDisplayName(_)
and description(_)
view modifiers. The simplest option here is a StaticConfiguration
(well, I haven’t learned about any others yet).struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: MyTimelineProvider()) { entry in
if #available(iOS 17.0, *) {
MyWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
MyWidgetView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
The StaticConfiguration
struct takes the widget’s kind
string, an instance of your timeline provider, and a closure to call with each entry in the timeline. The closure should return the SwiftUI view configured/rendered for that particular entry.
You can also provide the supportedFamilies
view modifier with a list of the different types of widget sizes that this widget supports, including Lock Screen widgets. You can use the environment variable .widgetFamily
inside the view to change the layout of the view based on what size widget is currently displayed:
@Environment(\.widgetFamily) var widgetFamily
Apps built using the iOS 17 SDK require all widgets to use the new containerBackground
modifier, which automatically handles padding.
Widgets are simple to use with SwiftUI previews: you can either preview the widget View
by itself, passing a static timeline entry, such as using the pre-Xcode 15 preview provider:
struct MyWidgetView_Previews: PreviewProvider {
static var previews: some View {
MyWidgetView(entry: MyWidgetEntry(date: .now, text: "Text"))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
Or you can use Xcode 15’s new #Preview
macro, with the version specifically designed for widgets, that accepts a timeline of entries. This time you pass it the widget itself, not the view the widget renders:
#Preview(as: .systemSmall) {
MyWidget()
} timeline: {
MyWidgetEntry(date: .now, text: "Text 1")
MyWidgetEntry(date: .now, text: "Text 2")
}
If you’ve reached this far, then you’ve done enough to design the widget and how it populates its timeline into the future, but we still need to tell iOS about it. This is done with one last struct.
To tell iOS about the available widgets in your app, you need a single widget bundle defined in the widget extension, which is a struct conforming to WidgetBundle
, and marked with the @main
wrapper. Similar to SwiftUI views, this requires one computed parameter, body
, but this time is of type some Widget
:
@main
struct MyWidgets: WidgetBundle {
var body: some Widget {
MyWidget()
}
}
Multiple different widgets can be returned, just put each on a new line within the body
. You can also do some logic here, such as if #available
checks to limit certain widgets to particular iOS versions, etc.
If you have multiple widgets that need the same data, you can reuse the same Timeline Entry and Timeline Provider in multiple Widget
structs.
With that, your app should be able to provide one or more widgets to the user, and control what sizes they are available in. However, you can’t yet provide options for the user to pick from, allowing them to “edit” the widget. I’ll write up what I know about that in another post, soon!
My latest LEGO review is up on Brickset, of the Disney 100 Enchanted Treehouse.
The star of the set is clearly the impressive selection of minidolls. Their shape definitely suits the Disney Princesses more than a minifigure would; it is just a shame their articulation is significantly less.
The two halves of the treehouse look fantastic together, and there’s plenty of play value with the slide, stairs, zip wire, canoe, and various other smaller builds and interactive sections. It looks good both on display and during play.
I recently came across a situation where I wanted to match the background colour of a a header above a SwiftUI List
(using the default .insetGrouped
List style) to that used by the List itself. I had done no styling to the List itself, so was relying on the system-provided background colour—this is what I wanted to match.
I tried a couple of the standard constants provided by UIColor
, and landed on .secondarySystemBackground
. It wasn’t until I had the build running on my phone and I was using the app later in the day that I noticed something was off slightly:
In light mode, everything was fine; but in dark mode, the background of the header and navigation toolbar wasn’t dark enough! It turns out that what I actually wanted was .systemGroupedBackground
:
Now they match up as intended. Let this be a lesson to myself to test in both light mode and dark mode when developing anything that relies on colour!
I am just loving the Creator 3-in-1 sets that LEGO are churning out nowadays:
31138 Beach Camper Van is a perfect example of this: an excellent camper van, some lovely little beach huts, an adorable crab—and that’s all just in the primary build! Let’s take a look at the set in detail, including the two alternative models that often make the 3-in-1 sets the success they are.
I love LEGO’s Creator 3-in-1 range, where every set can be built into three different designs. 31139 Cozy House is no exception, with three excellent builds, and a handful of adorable little bugs to boot!
The primary model of the set is the titular Cozy House (or “cosy”, as we’d be more likely to write here in the UK). It’s a fantastic little two-story building with pitched roofs, a delightful garden, and is full of excellent details!
The latest batch of LEGO I’ve been provided to review included three City Stuntz sets, a range that I’ve not built or played with before. They come with flywheel-powered bikes and stunt arenas, and are all pretty creative and fun! The first review went up today, for 60359 Dunk Stunt Ramp Challenge:
Despite the theme entering its third year, this was my first experience of the line, and I was quite impressed! The flywheel-powered bike is a lot of fun, and it kept both my children (aged four and six) entertained for some considerable time (once they stopped fighting over whose turn it was).